user1000952
user1000952

Reputation:

How to mask input with an Angularjs directive

I've been trying to create an AngularJS directive that will accept user input and display it as bullet points, just like a password input would.

This is what I have so far:

expose.link = function(scope, element, attributes, controller) {

    var maskValue = function(value) {
        // replace all characters with the mask character
        return (value || "").replace(/[\S]/g, "\u2022");
    }

    controller.$parsers.push(function(value) {
        return maskValue(value);
    });

}

Which does this:

View: asdf Model: ****

But I actually need it to do this:

View: **** Model: asdf

I've also tried this:

controller.$formatters.push(function(value) {
    return maskValue(value);
});

But this only works if the model is changed from my code. I need it to work as the user types into the input field.

I feel like this could work if there was a way that I could manually trigger the $formatters to run, but I couldn't find a way to do this. There may be something obvious I'm missing though.

Upvotes: 7

Views: 8814

Answers (3)

navid
navid

Reputation: 343

this is another directive: you can save password in a variable in controller scope and then mask input value to star:

module.directive("customPassword", function () {
    return {
        restrict: "A",
        scope:{
            customPassword: '='
        },
        require: "ngModel",
        link: function (scope, element, attrs, ngModel) {
            ngModel.$parsers.unshift(function (value) {
                if(!value || value.length==0)
                    scope.customPassword = "";
                else if (value.length <scope.customPassword.length)
                    scope.customPassword = scope.customPassword.substring(0, scope.customPassword.length-1);
                else
                    scope.customPassword = scope.customPassword +value.substring(value.length-1);

                var star = (value || "").replace(/[\S]/g, "\u2022");
                element.context.value = star;
            });
        }
    };
});

and in view:

<input type="text" class="form-control" id="testpass" name="testpass" 
    ng-model="testpass"  custom-password="passValue" >

Upvotes: 0

user1000952
user1000952

Reputation:

In the end the solution was pretty simple...

angular.module("core.application.main")
.directive("core.application.main.directive.mask",
    ["$compile",
    function($compile) {

        // Mask user input, similar to the way a password input type would
        // For example: transform abc123 into ******
        // Use this when you need to mask input, but without all the baggage that comes with the
        // 'password' input type (such as the browser offering to save the password for the user)
        //
        // ----- POSSIBLE USE CASE -----
        // CVC input field for a credit card
        //
        // ----- HOW IT WORKS -----
        // When the input field gains focus, it's cloned and the clone is changed to a 'password' type
        // The original input field is hidden at this point and the clone is shown
        // When the user types into the clone the real input field's model will be kept up to date
        // When the cloned input field loses focus, it's removed from the page and the original input
        // is shown. The model value is masked for display in this field using the $formatters

        var expose = {};
        expose.require = "ngModel";
        expose.restrict = "A";

        expose.link = function(scope, element, attributes, controller) {

            var maskedInputElement;

            var maskValue = function(value) {
                // replace all characters with the mask character
                return (value || "").replace(/[\S]/g, "\u2022");
            };

            var createMaskedInputElement = function() {
                if (! maskedInputElement || ! maskedInputElement.length) {
                    maskedInputElement = element.clone(true);
                    maskedInputElement.attr("type", "password"); // ensure the value is masked
                    maskedInputElement.removeAttr("name"); // ensure the password save prompt won't show
                    maskedInputElement.removeAttr("core.application.main.directive.mask"); // ensure an infinite loop of clones isn't created
                    maskedInputElement.bind("blur", function() {
                        element.removeClass("ng-hide");
                        maskedInputElement.remove();
                        maskedInputElement = null;
                    });
                    $compile(maskedInputElement)(scope);
                    element.after(maskedInputElement);
                }
            };

            element.bind("focus", function() {
                createMaskedInputElement();
                element.addClass("ng-hide");
                maskedInputElement[0].focus();
            });

            controller.$formatters.push(function(value) {
                // ensure the displayed value is still masked when the clone is hidden
                return maskValue(value);
            });

        };

        return expose;
    }
]);

And the test for it...

describe("Mask Directive", function() {
    var scope;
    var element;
    beforeEach(module("core.application.main"));
    beforeEach(inject(function($rootScope, $compile) {
        scope = $rootScope.$new();
        /* jshint multistr: true */
        element = angular.element(" \
            <input name='cvc' type='text' ng-model='cvc' class='test' core.application.main.directive.mask> \
        ");
        $compile(element)(scope);
    }));

    it("should create a masked clone of the element when its focussed", function() {
        var clonedElement;
        element.triggerHandler("focus");
        clonedElement = element.next();
        expect(clonedElement.attr("type")).toEqual("password");
        expect(clonedElement.attr("name")).toEqual(null);
        expect(clonedElement.attr("core.application.main.directive.mask")).toEqual(null);
        expect(clonedElement.attr("ng-model")).toEqual("cvc");
        expect(clonedElement.hasClass("test")).toEqual(true);
        expect(element.hasClass("ng-hide")).toEqual(true);
    });

    it("should not create a masked clone if the mask has already been created", function() {
        expect(element.next().length).toEqual(0); // the clone shouldn't exist yet
        expect(element.next().next().length).toEqual(0); // another potential clone shouldn't exist yet
        element.triggerHandler("focus");
        expect(element.next().length).toEqual(1); // a clone should have been created
        element.triggerHandler("focus");
        expect(element.next().next().length).toEqual(0); // another clone should not have been created
    });

    it("should remove the masked input on blur and show the element", function() {
        var clonedElement;
        element.triggerHandler("focus");
        clonedElement = element.next();
        clonedElement.triggerHandler("blur");
        expect(element.next().length).toEqual(0);
        expect(element.hasClass("ng-hide")).toEqual(false);
    });

    it("should keep the element's model up to date based on the masked elements value", function() {
        var clonedElement;
        element.triggerHandler("focus");
        clonedElement = element.next();
        clonedElement.val("test").triggerHandler("input");
        expect(scope.cvc).toEqual("test");
        expect(element.val()).toEqual("••••");
    });

});

If you want to use this then you'll need to rename "core.application.main.directive.mask" to whatever namespacing method you use.

A colleague questioned the need for this directive, suggesting to just use the plain password field without the 'name' attribute... unfortunately something needs the 'name' attribute, otherwise the validation on the field won't work in Angular.

Upvotes: 3

Vaibhav Pachauri
Vaibhav Pachauri

Reputation: 2671

Is there any problem with making the type of input element as password ?

<input type="password" data-ng-model="maskedValue">

I guess this will do the trick. Or is there any other requirement that I have missed ?

Upvotes: 0

Related Questions