bendrucker / angular-credit-cards

Angular directives for parsing and validating credit card inputs
MIT License
328 stars 98 forks source link

Validate only once type length is correct? #20

Closed owlyowl closed 9 years ago

owlyowl commented 9 years ago

Hi just wondering if it was possible to expose credit card type/length information so we could kick off validation only once the user has entered the minimum number of characters for a given card type.

owlyowl commented 9 years ago

Or would it be possible to inject the types provided by the angular credit cards library in to a directive outside of the project?

bendrucker commented 9 years ago

so we could kick off validation only once the user has entered the minimum number of characters

That's not an option with the way ngModelController works. Validation is run every time the model changes. There's no possibility of "kicking off" validation when a particular condition is met, short of dynamically adding a validation rule (terrible idea).

What it sounds like you want to do is not show an error message until it's clear that the data is invalid and the user is finished entering it. Personally I use the ngModelController.$touched property with ngIf to hide the ngMessages until the field has been blurred.

If you can walk me through the exact UI you're trying to create I can better understand whether there needs to be a feature here.

owlyowl commented 9 years ago

Hi Ben, All good I implemented it like so:

link: ($scope: ng.IScope, element: JQuery, attrs: any, ngModel: any) => {
                if (!ngModel) return;

                var minCardTypeLengths = {
                    'Visa': 13,
                    'MasterCard': 15,
                    'American Express': 15,
                    'Diners Club': 14,
                    'Discover': 16,
                    'JCB': 16,
                    'UnionPay': 16
                }

                var form: ng.IFormController = ngModel[1];

                var inputName: string = element.attr("name");
                var dirtySet: boolean = false;
                var validationApplied: boolean = false;

                $scope.$watch(attrs.cardViewModel, function (value:
string[]) {
                    var preliminaryCardType =
form['cardNumber'].$ccEagerType;
                    var minCardLengthBeforeValidation = 16;
                    if (preliminaryCardType && preliminaryCardType.length >
0) {
                        minCardLengthBeforeValidation =
minCardTypeLengths[preliminaryCardType] || 16;
                    }

                    if ((value && value.length > 0 && value.join('').length
>= 0)) {
                        ngModel[0].$setViewValue(value.join(''));
                        ngModel[0].$render();

                        if (!dirtySet) {
                            ngModel[0].$pristine = false;
                            ngModel[0].$dirty = true;
                            ngModel[0].$setTouched();
                            dirtySet = true;
                        }

                        if (value.join('').length >=
minCardLengthBeforeValidation) {
                            validationApplied = true;
                            if (form[inputName].$invalid) {

element.closest('.input-wrap').removeClass('is-valid');

element.closest('.input-wrap').addClass('is-invalid');
                            } else {

element.closest('.input-wrap').removeClass('is-invalid');

element.closest('.input-wrap').addClass('is-valid');
                            }
                        } else if(validationApplied) {

element.closest('.input-wrap').removeClass('is-valid');

element.closest('.input-wrap').removeClass('is-invalid');
                        }

                    } else if(dirtySet) {
                        console.log('removing class');

element.closest('.input-wrap').removeClass('is-valid');

element.closest('.input-wrap').removeClass('is-invalid');
                    }
                }, true);
            }

it's a bit dirty for now but will clean it up later

bendrucker commented 9 years ago

Please post this stuff from GitHub in the future since it's impossible to read w/ your mail client's formatting.

Definitely have to advise very strongly against shipping something like this. It's incredibly brittle. You're basically undoing a huge chunk of Angular's form handling.

As mentioned, I want to encourage you to think about validations the way Angular does. Validations are just a set of functions that return true/false. The place for complex logic is in determining how to convey validation state to the user and apply it to your interface.

owlyowl commented 9 years ago

Hi Ben,

I was trying to find a neater way to do it but I have 4 text boxes which take the credit card input which then populate a hidden with a single string representing the credit card number as a combination of all 4 inputs.

The problem I had was that angular doesn't set the hidden as being dirty or touched or anything similar when it gets updated from the view:

<div class="field-wrap cc-number">
       <label for="card-number">
            Card number<br />
            <span class="cc-icon {{ creditCardForm.cardNumber | creditCardIconFromType }}"></span>
            <div class="input-wrap input-border" data-auto-tab=".group4">
                  <input type="text" pattern="[0-9]*" size="4" maxlength="4" class="group4" id="cardNumber1" name="cardNumber1" data-ng-model="payment.viewCardNumber[0]" data-only-numbers placeholder="XXXX" />
                  <input type="text" pattern="[0-9]*" size="4" maxlength="4" class="group4" id="cardNumber2" name="cardNumber2" data-ng-model="payment.viewCardNumber[1]" data-only-numbers placeholder="XXXX" />
                  <input type="text" pattern="[0-9]*" size="4" maxlength="4" class="group4" id="cardNumber3" name="cardNumber3" data-ng-model="payment.viewCardNumber[2]" data-only-numbers placeholder="XXXX" />
                  <input type="text" pattern="[0-9]*" size="4" maxlength="4" class="group4" id="cardNumber4" name="cardNumber4" data-ng-model="payment.viewCardNumber[3]" data-only-numbers placeholder="XXXX" />
                  <input type="hidden" data-ng-model="payment.creditCardNumber" data-card-view-model="payment.viewCardNumber" data-cc-number data-cc-eager-type="payment.creditCardType" name="cardNumber" data-ng-model-options="{ allowInvalid: true }" />
                  <span data-ng-show="creditCardForm.cardNumber.$touched || creditCardForm.cardNumber.$dirty" class="input-message-validate">
                  <span data-ng-messages="creditCardForm.cardNumber.$error" data-ng-show="creditCardForm.cardNumber.$valid === false">
                      <span data-ng-message="ccNumber">Bad credit card #</span>
                  </span>
                          <i class="icon-tick" data-ng-show="creditCardForm.cardNumber.$valid === true"></i>
                      </span>
                  </div>
        </label>
</div>
bendrucker commented 9 years ago

Not sure why it's not set as dirty. Seems like a lot of effort here for something that's probably frustrating your users. The 4 inputs would piss me off as a user.

owlyowl commented 9 years ago

Yeah I couldn't figure out why it wasn't being set as dirty. I was thinking of spiking it in to a codepen or plunkr or something and having more of a look

owlyowl commented 9 years ago

http://plnkr.co/edit/Cr0aSShpXvxxK77E8Qgw?p=preview

This is the current implementation.. if you remove the manual setting of dirty and things like that you'll see it doesn't update. I think it is because the field is a hidden field and maybe needs to be a text input with display: none

bendrucker commented 9 years ago

http://stackoverflow.com/a/18446730/906162