17

I've seen this post - it shows one possible solution. But I would like to have a more elegant way of doing masked input.

It should also play nicely with knockout validation plugin (or maybe extending it).

Anyone know how is there similar project out there?

Community
  • 1
  • 1
kyrisu
  • 4,541
  • 10
  • 43
  • 66

5 Answers5

33

If you wanted to use the excellent Masked Input Plugin in Knockout, it's pretty easy to write a basic custom binding rather than an extender.

ko.bindingHandlers.masked = {
    init: function(element, valueAccessor, allBindingsAccessor) {
        var mask = allBindingsAccessor().mask || {};
        $(element).mask(mask);
        ko.utils.registerEventHandler(element, 'focusout', function() {
            var observable = valueAccessor();
            observable($(element).val());
        });
    }, 
    update: function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        $(element).val(value);
    }
};

And then in your HTML:

<input type="text" data-bind="masked: dateValue, mask: '99/99/9999'" />
<input type="text" data-bind="masked: ssnValue, mask: '999-99-9999'" />

And so on with various masks. This way, you can just put the mask right in your databinding, and it allows a ton of flexibility.

Jason Clark
  • 482
  • 4
  • 10
  • 2
    [An anonymous user comments](http://stackoverflow.com/review/suggested-edits/2574505) that the `registerEventHandler` should be on the `blur` event not `focusout`. – Rup Jul 24 '13 at 09:30
  • 1
    +1 - Great answer - I just added this to my project and it works like a charm. – David Robbins Aug 28 '13 at 21:20
  • If you initialize data from the view model, it does not show the input mask until you click into the input. For example, it would show 999999999 for the SSN instead of 999-99-9999. – Mike Cole Apr 02 '14 at 18:15
  • I'm getting an error with `var observable = valueAccessor()` which is actually just the string value of my mask. Any suggestions? – Mark B Jul 28 '14 at 18:37
  • 1
    This is a great solution! I am encountering an issue that I can't seem to find a solution to, however. If I use the code riceboyler contributed, and I use an optional section in my mask ('99999?-9999'), the literal and placeholders appear for the optional section when the page loads. (data is '90210', control reads '90210-____'). If I enter and leave the control, the mask behaves as expected. To fix this, I added `$(element).trigger('blur');` to the end of the update method. – jfw Jan 09 '15 at 19:32
  • thanks for a clever thought about value updating on focusout / blur event. My masked input suddenly did not update value observable. – blazkovicz Aug 27 '15 at 16:21
  • 1
    @MarkB you should use valueAccesssor() for mask and allBindingsAccessor().value or allBindingsAccessor().textInput for observable, also update method in example is redundant and also updates element with wrong value. – blazkovicz Aug 27 '15 at 16:24
  • This plug currently has a lousy interface at least in the current version of Chrome. If you tab into a field it is fine, but if you click in the middle of an empty field the caret ends up in the middle of the masked field and typing just becomes a mess. Even in the demo on the plugin's own page fails. – Simon_Weaver Nov 20 '17 at 20:07
  • Yeah, it probably hasn't been updated in 2 or 3 years. I believe the original engineer has moved on to new libraries. – Jason Clark Nov 21 '17 at 00:17
6

Well done, riceboyler. I took your code and extended it a little in order to use the "placeholder" property of the Masked Input Plugin:

    ko.bindingHandlers.masked = {
        init: function (element, valueAccessor, allBindingsAccessor) {
            var mask = allBindingsAccessor().mask || {};
            var placeholder = allBindingsAccessor().placeholder;
            if (placeholder) {
                $(element).mask(mask, { placeholder: placeholder });
            } else {
                $(element).mask(mask);
            }
            ko.utils.registerEventHandler(element, "blur", function () {
                var observable = valueAccessor();
                observable($(element).val());
            });
        },
        update: function (element, valueAccessor) {
            var value = ko.utils.unwrapObservable(valueAccessor());
            $(element).val(value);
        }
    };

HTML with placeholder:

    <input id="DOB" type="text" size="12" maxlength="8" data-bind="masked: BirthDate, mask: '99/99/9999', placeholder: 'mm/dd/yyyy', valueUpdate: 'input'"/>

HTML without placeholder:

    <input id="DOB" type="text" size="12" maxlength="8" data-bind="masked: BirthDate, mask: '99/99/9999', valueUpdate: 'input'"/>

The KO binding works either way.

hoyt1969
  • 81
  • 1
  • 3
  • How did you decided to bind to the 'blur' event vs 'focusout'? It fixed an annoying issue for me where non-required blank fields were being marked as not valid (using KO Validation). For example, a blank non-required phone number's value sometimes would be (___) ___-____ using focusout (and therefore not valid). For blur it was always blank. Anyways, thank you. – Jibran May 27 '17 at 04:24
2

Just take the code from the answer in that link and put it in a extender (Written on free hand, can have errors)

ko.extenders.masked = function(observable, options) {
    return ko.computed({
        read: function() {
            return '$' + this.observable().toFixed(2);
        },
        write: function(value) {
            // Strip out unwanted characters, parse as float, then write the raw data back to the underlying observable
            value = parseFloat(value.replace( /[^\.\d]/g , ""));
            observable(isNaN(value) ? 0 : value); // Write to underlying storage
        }
    });
};

edit: You probably want to supply the mask as a options instead of having it hardcoded to USD etc

update: If you want to use the mask plugin from riceboyler's answer but with extenders you can do

ko.extenders.mask = function(observable, mask) {
    observable.mask = mask;
    return observable;
}


var orgValueInit = ko.bindingHandlers.value.init;
ko.bindingHandlers.value.init = function(element, valueAccessor) {
    var mask = valueAccessor().mask;
    if(mask) {
        $(element).mask(mask);
    }

    orgValueInit.apply(this, arguments);
}

http://jsfiddle.net/rTK6G/

Anders
  • 17,306
  • 10
  • 76
  • 144
  • Your first solution is really great. It helped me to achieve masking functionalty with out using masking plugin :) Thx a lot :) – Biki May 15 '14 at 16:07
2

I tried to use the first answer but it did not work with ko.validation plug in. My validation errors were not being displayed.

I wanted to have little bit more intuitive ko binder. Here is my solution. I am using jquery.inputmask plug in. I also wipe out the property on my viewmodel if not value entered.

    ko.bindingHandlers.mask = {
        init: function (element, valueAccessor, allBindingsAccessor, viewModel,     bindingContext) {
            var mask = valueAccessor() || {};
            $(element).inputmask({ "mask": mask, 'autoUnmask': false });
            ko.utils.registerEventHandler(element, 'focusout', function () {
                var value = $(element).inputmask('unmaskedvalue');            
                if (!value) {
                    viewModel[$(element).attr("id")]("");                
                }
            });
        }
    };

Here is the usage:

<input type="text" data-bind="value: FEIN, mask: '99-9999999'" id="FEIN" >
Ajay2707
  • 5,690
  • 6
  • 40
  • 58
benk
  • 51
  • 1
0

You can use this homemade solution, work perfectly for me:

My Binding knockout call masked inspired from the net, i added some managed language and update from different event. Also I use this js library for using basically : https://plugins.jquery.com/maskedinput/

You can see in my binding the term "allBindingsAccessor().mask" this is from the library maskedinput

 ko.bindingHandlers.masked = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        var mask = allBindingsAccessor().mask || {},
        getCaretPosition,
        setCaretPosition;

        // Permet d'obtenir la position du curseur
        getCaretPosition = function getCaretPosition(element) {
            // Initialise la position
            var caretPos = 0, sel;

            // IE 
            if (document.selection) {
                // Donne le focus à l'élément
                element.focus();
                // Afin d'obtenir la position du curseur
                sel = document.selection.createRange();
                // On place le curseur à 0
                sel.moveStart('character', -element.value.length);
                caretPos = sel.text.length;
            }
                // Firefox 
            else if (element.selectionStart || element.selectionStart === '0') {
                caretPos = element.selectionStart;
            }
            return (caretPos);
        };

        // Permet de définir la position du curseur en fonction d'une position donnée
        setCaretPosition = function setCaretPosition(element, pos) {
            var range;
            if (element.setSelectionRange) {
                element.focus();
                element.setSelectionRange(pos, pos);
            }
            else if (element.createTextRange) {
                range = element.createTextRange();
                range.collapse(true);
                range.moveEnd('character', pos);
                range.moveStart('character', pos);
                range.select();
            }
        };

        // Définition du masque inséré dans le champ
        if (configSvc.culture === "fr-FR") {
            // Cas francais
            $(element).mask("99/99/9999", { placeholder: "JJ/MM/AAAA" });
        }
        else {
            // Cas anglophone
            $(element).mask("99/99/9999", { placeholder: "MM/DD/YYYY" });
        }

        // On capte l'événement d'appuie sur une touche 
        ko.utils.registerEventHandler(element, 'keypress', function () {
            var observable = valueAccessor(),
                position;
            // Afin de résoudre le pb de déplacement du curseur a la fin du mask lors de la mise à jour de l'observable knockout
            if ($(element).val().length === 10) {
                // On récupère la dernière position
                position = getCaretPosition(this);
                // On met à jour la valeur de l'obersable (en cas de sauvegarde) 
                observable($(element).val());
                // On force la position du curseur apres mise à jour de l'observable à la derniere position récupéré
                setCaretPosition(this, position);
            }
        });

        // On capte l'événement de perte de focus pour mettre l'obersable à jour
        ko.utils.registerEventHandler(element, 'blur', function () {
            var observable = valueAccessor();
            observable($(element).val());
        });

        // On capte l'événement change pour mettre l'obersable à jour
        ko.utils.registerEventHandler(element, 'change', function () {
            var observable = valueAccessor();
            observable($(element).val());
        });
    },
    update: function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        $(element).val(value);
    }

};

in my html page, i use this observable "masked" :

<input type="text" id="head-birthDate" class="form-control" data-bind="masked: birthDate" />

Finally in my js :

birthDate is just an observable

this.birthDate = ko.observable();
Yvan
  • 1,081
  • 2
  • 20
  • 27