jonsamwell/angular-auto-validate

Help with custom validattor

Closed this issue · 17 comments

Good morning. I'm trying to write a custom phone validator that calls a web API that uses Google's phones library to validate the phone number.
I've got the back end working fine but am having issues with the directive.
What I have so far is based on the code in the demo. Here it is
app.directive('validatephone', ['ValidatePhoneService',
function (ValidatePhoneService) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var validateFn = function (viewValue) {
if (ctrl.$isEmpty(viewValue)) {
ctrl.$setValidity('validatephone', false);
return undefined;
}

                ValidatePhoneService.get({ phone: viewValue }).$promise.then(function (result) {
                    if (result.IsPhoneValid) {
                        ctrl.$setViewValue(result.PhoneNumber);
                        ctrl.$setValidity('validatephone', true);
                        return result.PhoneNumber;
                    } else {
                        ctrl.$setValidity('validatephone', false);
                        return undefined;
                    }
                });
            };

            ctrl.$parsers.push(validateFn);
            ctrl.$formatters.push(validateFn);
        }
    }
}]);

And I use it like this:

Note: We are using Angular 1.2, not 1.3.
I'm having two issues.

  1. Even though I have UpdateOn set to blur (which worked for required validation before I started this) it is calling the directive on each key press. I'm assuming there is something I need to add in the directive to ignore calls until blur? I don't know how to do that.
  2. I want to return the formatted phone number from the service to the text box. I'm calling
    ctrl.$setViewValue(result.PhoneNumber);
    where result.PhoneNumber is the corrected phone number and have confirmed that $viewValue is being set but the text box does not change.

Any thoughts or suggestions on how to fix these two?

Hi,

Could you provide a plunkr so I can test it out?

Just mock the ValidatePhoneService to return a valid phone number and I'll be able to see what is going on :-)

Sure thing. Here you go
http://plnkr.co/edit/Ri5LOss3zod0u8DADFrS
I have some comments in there to guide and a console.log to show how it's being called multiple times.

Thanks, I'll take a look :-)

I've been playing with this some more and have discovered that when the directive is applied to the text box setting the scope value in the controller has no affect. i.e. if from the controller I set $scope.technicalContactPhone = 'foo'
foo is not set in the textbox on page load. If I remove the directive from the text box it is. Something in the directive is breaking this.
Also if I try to manually set the scope value from the directive it just blanks out the textbox.
I've also tried setting elm.text(result.PhoneNumber); as the Angular docs show. That sets the text in the dob but the textbox / scope value do not change.
I have no idea what to do next. I must be missing something fundamental here.

Hi - I'm hopefully going to get time to look at this tonight :-) Sorry for
the hold up!

On Thu, Oct 9, 2014 at 5:15 PM, dsargent3220 notifications@github.com
wrote:

I've been playing with this some more and have discovered that when the
directive is applied to the text box setting the scope value in the
controller has no affect. i.e. if from the controller I set
$scope.technicalContactPhone = 'foo'
foo is not set in the textbox on page load. If I remove the directive from
the text box it is. Something in the directive is breaking this.
Also if I try to manually set the scope value from the directive it just
blanks out the textbox.
I've also tried setting elm.text(result.PhoneNumber); as the Angular docs
show. That sets the text in the dob but the textbox / scope value do not
change.
I have no idea what to do next. I must be missing something fundamental
here.


Reply to this email directly or view it on GitHub
#18 (comment)
.

I've got part of it figured out.
Because I'm calling a service with a promise like so
ValidatePhoneService.get({ phone: viewValue }).$promise.then(function (result)
. . . . .
});
That promise is not getting returned immediately, instead as JS is designed to do, in that case undefined is returned.
If I put a return in front of the above it returns an object with a promise.
Still not sure how to proceed here.
Thanks for looking at it!

Sorry for the delay been so busy - I'm definitely getting round to looking at this at the weekend!

Just had another thought. There is a piece of undocumented functionality in the module which allows you to set validation from an external source -pretty much what you are doing i.e. an api call (it works well it just I haven't written test for it yet so it not officially released!)

I added a method to the form controller to apply external validation to model items.

The method on the form controller is:

frmCtrl.setExternalValidation = function (modelProperty, errorMsgKey, errorMessageOverride) {
    var success = false;
    if (frmCtrl[modelProperty]) {
        frmCtrl[modelProperty].setExternalValidation(errorMsgKey, errorMessageOverride);
        success = true;
    }
    return success;
};

It takes in the name of the control i.e.

<input type=“text” name”firstname” ng-model=“model.firstname” required min-length=“3” /></code>)

which should probably correspond to the ng-model property.
The errorMsgKey i.e. the key of the validation error that is able to be looked up from the error message provider - although this can be blank.
If it is blank or unknown you can provide the actual error message you want to display in the errorMessageOverride parameter.

So the call would look something like this:

frmCtrl.setExternalValidation('firstname', undefined, ’some error message to the user’);

or

frmCtrl.setExternalValidation('firstname', ‘required');

So in your controller the code to apply server validation after the from has been submitted would look something like this:

app.controller('demoCtrl', [
    '$scope',
    function ($scope) {
        $scope.user = {};

        $scope.submit = function (frmCtrl) {
            $http.post('https://api.app.com/users', $scope.user).then(function (response) {
                if (response.data.validationErrors) {
                    angular.forEach(response.data.validationErrors, function (error) {
                        frmCtrl.setExternalValidation(error.key, error.messageKey, error.message);
                    })
                }
            });
        };
    }
]);

Your view would look something like this

<form role="form" name=“singupFrm" novalidate="novalidate" ng-submit="submit();">
                <div class="form-group">
                    <label class="control-label">Name</label>
                    <input type="text" name="firstname" class="form-control" placeholder="Enter Name" ng-model="user.name" required="required" />
                </div>
                <div class="form-group">
                    <label class="control-label">Email address</label>
                    <input type="email" name=“email" class="form-control" placeholder="Enter email" ng-model="user.email" required="required" />
                </div>
                <div class="form-group">
                    <label class="control-label">Password</label>
                    <input type=“password" name=“password" class="form-control" ng-model="user.password" placeholder="Password" required="required" ng-minlength="6" />
                </div>

                <button type="submit" class="btn btn-default">Submit</button>
</form>

I hope this all make sense, it might be something worth exploring?

Jon

Did you have any luck with this approach?

Sorry. I was on vacation for over a week and haven't had time to get back to that. I looked at it briefly and must confess it didn't make allot of sense, but I'll try and take another look tomorrow. Can't today. Thanks!

Sent from my iPhone

On Nov 11, 2014, at 2:48 AM, Jon Samwell notifications@github.com wrote:

Did you have any luck with this approach?


Reply to this email directly or view it on GitHub.

Hello, my apologies for not getting to this until now. Got the rest of the project done and am now looping back to this.
I'm not sure where I'm supposed to get access to frmCtrl. I'm sure I'm being dense here but would love any help you can give.

To clarify a little further. I have a directive for the phone number I've created. Here's the beginning of the definition.

var directive = {
    scope: {
        label:"@",
        id: "@",
        required:"=?", // Are fields required.
    },
    link: link,
    restrict: 'E',
    require: 'ngModel',
    templateUrl: 'app/rma/directives/selPhoneNumber.html'
};
return directive;

function link(scope, element, attrs, ctrl) {
    console.log(ctrl);
    ctrl.setExternalValidation('technicalContactPhone', 'validatephone');

The ctrl.setExternalValidation call returns an error saying it is not a function . . .BUT the console log for ctrl shows that the function exists on the object (I am baffled).

The call to this directive looks like this.

<sel-phone-number label="Country Code and Phone Number" name="technicalContactPhone" id="technicalContactPhone" ng-model="model.technicalContactPhone" required="true"></sel-phone-number>

The problem I hope this solves is that I'm setting the validity in the directive like this
ctrl.$setValidity('validatephone', false);

And have a custom error message set up

defaultErrorMessageResolver.getErrorMessages().then(function (errorMessages) {
    errorMessages.validatephone = 'Please enter a valid phone number, including Country Calling Code and Area Code';
    errorMessages.serialNumberRequired = 'Serial number is required. If your product does not have a serial number, enter "none"';
});

Note; the serialNumberRequired message works fine.

But when the validity is set to false, nothing happens (outwardly). The dom is changed with the correct classes you would expect, but Auto Validate is not doing anything with them. I feel like I'm missing something really stupid and can't see it.

Hmmm, I'll try a create a example to see if something odd is going on. What is the html of the directive's template? It might not be in the structure the element modifier is expecting so it is missing out putting the error message in.

In the meantime it would be worth putting a debugger; statement here
https://github.com/jonsamwell/angular-auto-validate/blob/master/src/services/defaultErrorMessageResolver.js#L167

To see if the error message resolve is actually finding that error message or even getting to that point in the code?

Here is the full directive:

(function() {
    'use strict';

    angular
        .module('rmaApp')
        .directive('selPhoneNumber', selPhoneNumber);

    selPhoneNumber.$inject = ['validatePhoneService'];

    function selPhoneNumber(validatePhoneService) {
        // Usage:
        //     <sel-phone-number id="SomeId" ng-model="{countryCallingCode: '', phoneNumber:''}" required="true"></sel-phone-number>
        // Creates:
        // 
        var directive = {
            scope: {
                label:"@",
                id: "@",
                required:"=?", // Are fields required.
            },
            link: link,
            restrict: 'E',
            require: 'ngModel',
            templateUrl: 'app/rma/directives/selPhoneNumber.html'
        };
        return directive;

        function link(scope, element, attrs, ctrl) {
            console.log(ctrl);
            console.log(ctrl.setExternalValidation);
            // Below throws an error.
            ctrl.setExternalValidation('technicalContactPhone', 'validatephone', 'foo');

            // If user does not pass in required, then assume it is not required (if we don't do this, it will be undefined)
            if (scope.required === undefined) {
                scope.required = false;
            }

            // Functions
            scope.checkPhone = checkPhone;

            // Checks the phone values when there is a data load into the field.
            scope.$watch(function () { return scope.phoneNumber; }, function (newValue, oldValue) {
                if (oldValue === '' && newValue !== '') {
                    checkPhone();
                }
            });

            ctrl.$render = function () {
                scope.countryCallingCode = ctrl.$viewValue.countryCallingCode;
                scope.phoneNumber = ctrl.$viewValue.phoneNumber;
            };

            // Run check on load.
            checkPhone();

            function checkPhone() {
                if (angular.isDefined(scope.phoneNumber) && scope.phoneNumber.length > 0) {
                    var phone = parsePhone(scope.countryCallingCode, scope.phoneNumber);
                    validatePhoneService.get({ phoneccc: phone.countryCallingCode, phone: phone.phoneNumber }).$promise.then(function (data) {
                        if (data.Valid) {
                            scope.countryCallingCode = data.CountryCallingCode;
                            scope.phoneNumber = data.Phone;
                            ctrl.$setViewValue({ countryCallingCode: scope.countryCallingCode, phoneNumber: scope.phoneNumber });
                            ctrl.$setValidity('validatephone', true);
                        } else {
                            console.log('set validity to false');
                            ctrl.$setValidity('validatephone', false);
                        }
                    });
                }
            }

            // Parse a phone number country calling code and phone number body into an object.
            // If there is no phone number country calling code, attempt to find it in the body.
            function parsePhone(countryCallingCode, phoneNumber) {
                // Split the phone number up into an array for later use.
                // The delimiter
                var d = '',
                    phone = [],
                    result = { countryCallingCode: '', phoneNumber: phoneNumber };
                // What delimiter is being used.
                if (phoneNumber.indexOf(' ') > -1) { d = ' '; }
                if (phoneNumber.indexOf('.') > -1) { d = '.'; }
                if (phoneNumber.indexOf('-') > -1) { d = '-'; }
                // Split the phone number into an array by the delimiter
                phone = phoneNumber.split(d);

                // Try to set country calling code to the form value
                result.countryCallingCode = countryCallingCode;
                if (angular.isDefined(result.countryCallingCode) && result.countryCallingCode !== '') {
                    // Remove the + from the ccc if there.
                    result.countryCallingCode = result.countryCallingCode.replace('+', '');
                } else {
                    // Set ccc to the first element in the array, that should be the country calling code
                    result.countryCallingCode = phone[0];

                    if (angular.isDefined(result.countryCallingCode) && result.countryCallingCode !== '') {
                        // Get rid of the + if it is there.
                        result.countryCallingCode = result.countryCallingCode.replace('+', '');
                        // Remove the ccc from the phone array then rejoin it using a period and set it to the element
                        phone.splice(0, 1);
                        result.phoneNumber = phone.join('.');
                    }
                }

                return result;
            }
        }
    }
})();

Here is the html template

<label class="control-label" for="{{id}}">{{label}}</label>
<div class="input-group">
    <input type="text" class="form-control" style="width:60px" ng-model="countryCallingCode" ng-required="required" tooltip="Country Calling Code.  Example: +1 for US" tooltip-trigger="focus" tooltip-placement="top">
    <span class="input-group-addon" style="width:0; border-left: 0; border-right: 0;"><b>.</b></span>
    <input type="text" class="form-control" id="{{id}}" ng-model="phoneNumber" ng-required="required" ng-blur="checkPhone()" tooltip="Phone Number.  Example: 123.555.1212" tooltip-trigger="focus" tooltip-placement="top">
</div>

And here is an example reference of me using it

<form name="frm" role="form" novalidate ng-submit="submit();">
<div class="panel panel-default" ng-show="userLoaded">
        <div class="panel-heading">
            <span class="h3">Technical Contact Information</span><br />
            (The person who experienced the failure and who may be contacted later by an SEL technician for further information.
        </div>
        <div class="panel-body">
            <div class="row">
    <div class="col-sm-4">
                    <div class="form-group">
                        <sel-phone-number label="Country Code and Phone Number" name="technicalContactPhone" id="technicalContactPhone" ng-model="model.technicalContactPhone" required="true"></sel-phone-number>
                    </div>
                </div>
            </div>
        </div>
    </div>
</form>

Here is the model for the technicalContactPhone property

var model = {
    technicalContactPhone: {countryCallingCode: '', phoneNumber:''}
};

The service will return something like the following.

{"Valid":true,"CountryCallingCode":"+1","Phone":"509.432.3220"}

Let me know if there is anything else I can help with

I did place a breakpoint on the line you indicated. It gets hit when the field is empty for the required attribute, but not for the phone validation.

Still banging my head against the wall on this one. Not sure what to try next.