1

I'm using jQuery validate for my form, and I have a custom rule for postal codes. I'm using an API to check the postal code and if it's correct, it has to fill the <input type="text" name="city"> field. I'm having this code:

$.validator.addMethod('postalcode', function(value, $elem) {
    var is_success = false;

    $.ajax({
        url: '/ajax/check_postal_code.php',
        data: {postal_code: value},
        json: true,
        async: false,
        success: function(msg) {
            is_success = msg.status === 'OK'

            if (is_success) {
                $('input[name="city"]').val(msg.results[0].address_components[1].long_name);
                $('input[name="city"]').valid();
            }
        }
    });

    return is_success;
}, 'Please enter a VALID postal code');

Now this executes the AJAX call correctly, however, it doesn't 'validate' the postalcode field. It only validates my address, captcha and city field (which is done by code).

If I remove the $('input[name="city"]').valid(); it validates everything, but doesn't detect the $('input[name="city"]').val(msg.results[0].address_components[1].long_name);. (it fills the input but gives an error that you need to insert the city).

How can I fill the city and validate it together with the whole form?


This is NOT a duplicate because my AJAX call is synchronous but when I manipulate an input and put data in it, jQuery validate won't see the change and say you have to insert a value. If I do validate the field manually after manipulating the field, some fields won't be validated anymore.

Joshua Bakker
  • 1,831
  • 2
  • 22
  • 52
  • Possible duplicate of [How do I return the response from an asynchronous call?](http://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) – Heretic Monkey Mar 15 '17 at 21:45
  • The problem is in the last part, it has nothing to do with the AJAX call being async because it isn't.. – Joshua Bakker Mar 16 '17 at 07:40

1 Answers1

1

I believe at least one of the problems was calling .valid() from within .addMethod(). Wrapping it in a setTimeout seems to work better.

https://codepen.io/anon/pen/mWqJbo?editors=1011

HTML:

<form id='addressForm'>
  <input id='postalcode' type='postalcode' name='postalcode' placeholder='postal code' />
  <br/>

  <input name='city' required placeholder='city' />
  <br/>

</form>
<button id='submit'>submit</button>

<div id='success' style='display:none'>Success</div>

JS:

var myUrl = "https://api.zippopotam.us/us/";
var postalcode = ''

$('input[name=postalcode]').on('change', function() {
  $('#success').hide();
  $(this).valid();
});

$('#submit').on('click', function() {
  $('#success').hide();
  if ($('#addressForm').valid()) {
    $('#success').show();
  }
});

//init jquery validation
$("#addressForm").validate({
  messages: {
    postalcode: 'Please enter a valid postal code'
  },
  rules: {
    postalcode: {
      required: true,
      postalcode: true
    },
    city: {
      required: true,
    }
  }
});

$.validator.addMethod('postalcode', function(value, $elem) {
  /* don't re-validate postalcode on submit - 
  saves time & prevent edits to the city field from being overwritten.
  If edits to city shouldn't be allowed, make city field readonly.
  Note that many zipcodes map to multiple cities (at least in the US).  You may want to present
  user with a selectable list in those cases.
  */
  if (postalcode && value === postalcode) {
    return true;
  }

  var is_success = false;
  var city = ''

  $.ajax({
    url: myUrl + $('input[name=postalcode]').val(),
    data: {
      postal_code: value
    },
    json: true,
    async: false,
    success: function(response) {
      is_success = typeof(response['post code']) !== 'undefined';
      city = response.places[0]['place name'];
    },
    error: function(err) {
      //zippopotam api returns 404 if zip is invalid
      if (err.status === 404) {
        //clear last valid postal code
        postalcode = '';
        $('input[name=city]').val('');
        $('#success').hide();
      }

      //fixme handle other errors / notify user
      console.log('error ' + JSON.stringify(err));
    }
  });

  if (is_success) {
    $('input[name=city]').val(city);

    //remember last valid postal code
    postalcode = $('input[name=postalcode]').val();

    //previous error message(s) may not be cleared if .valid() is called from within .addMethod()
    setTimeout(function() {
      $('input[name="city"]').valid();
    }, 1);
  }    
  return is_success;
}, 'Please enter a VALID postal code');

Your code uses a synchronous ajax which is generally not a good idea. If you wanted to make it async you would need to use the jquery validation remote method

However, that method assumes the ajax response will return "true" if the data is valid. You would need to be able to modify your backend so that there's a "valid" endpoint that will return true if the postal code is valid. I believe you would also need to add a second endpoint that returns the corresponding city.

Instead of making your backend conform to what remote requires, you could override it with a custom implementation.

Here's an example (codepen):

HTML:

<form id='addressForm'>
  <input id='postalcode' type='postalcode' name='postalcode'  placeholder='postal code'/>
  <br/>
  <input name='city' required placeholder='city'/>
  <br/>
</form>

<button id='submit'>submit</button>

<div id='success' style='display:none'>Success</div>

JS:

//override
$.validator.methods.remote = function(value, element, param, method) {
  //zippopotam specific
  param.url = myUrl + value;

  if (this.optional(element)) {
    return "dependency-mismatch";
  }

  method = typeof method === "string" && method || "remote";

  var previous = this.previousValue(element, method),
    validator, data, optionDataString;

  if (!this.settings.messages[element.name]) {
    this.settings.messages[element.name] = {};
  }
  previous.originalMessage = previous.originalMessage || this.settings.messages[element.name][method];
  this.settings.messages[element.name][method] = previous.message;

  param = typeof param === "string" && {
    url: param
  } || param;
  optionDataString = $.param($.extend({
    data: value
  }, param.data));
  if (previous.old === optionDataString) {
    return previous.valid;
  }

  previous.old = optionDataString;
  validator = this;
  this.startRequest(element);
  data = {};
  data[element.name] = value;
  $.ajax($.extend(true, {
    mode: "abort",
    port: "validate" + element.name,
    dataType: "json",
    data: data,
    context: validator.currentForm,
    success: function(response) {
      //zippopotam specific -> write your own definition of valid
      var valid = typeof(response['post code']) !== 'undefined',
        errors, message, submitted;
      ///////////////

      validator.settings.messages[element.name][method] = previous.originalMessage;
      if (valid) {
        //your code here
        $('input[name=city]').val(response.places[0]['place name'])

        //end your code 

        submitted = validator.formSubmitted;
        validator.resetInternals();
        validator.toHide = validator.errorsFor(element);
        validator.formSubmitted = submitted;
        validator.successList.push(element);
        validator.invalid[element.name] = false;
        validator.showErrors();
      } else {
        //your code here if api returns successfully with an error object

        //end your code
        errors = {};
        message = response || validator.defaultMessage(element, {
          method: method,
          parameters: value
        });
        errors[element.name] = previous.message = message;
        validator.invalid[element.name] = true;
        validator.showErrors(errors);
      }
      previous.valid = valid;
      validator.stopRequest(element, valid);
    },
    error: function(response) {
      //your code here if api fails on bad zip, e.g. zippopotam returns 404
      if (response.status === 404) {
        $('input[name=city]').val('');
        //end your code
        errors = {};
        message = validator.defaultMessage(element, {
          method: method,
          parameters: value
        });
        errors[element.name] = previous.message = message;
        validator.invalid[element.name] = true;
        validator.showErrors(errors);
        previous.valid = false;
        validator.stopRequest(element, false);
      } else {
        console.log('problem with validation server');
      }  
    }
  }, param));
  return "pending";
};

var myUrl = "https://api.zippopotam.us/us/";
//init jquery validation
$("#addressForm").validate({
  messages: {
    postalcode: 'Please enter a valid postal code'
  },
  rules: {
    postalcode: {
      required: true,
      remote: {
        url: myUrl,
        type: "get",
        data: ''
      }
    },
    city: {
      required: true,
    }
  }
});

$('input[name=postalcode]').on('change', function() {
  $('#success').hide();
  $('input[name=postalcode]').valid() 
})

$('#submit').on('click', function() {
  if ($("#addressForm").valid()) {
    $('#success').show();
  } else {
    $('#success').hide();
  }
})
imjosh
  • 4,167
  • 1
  • 16
  • 22
  • Won't the second part of your answer (where you talk about editing `remote` do that for every rule that uses remote? Because I need to use remote for captcha too. – Joshua Bakker Mar 17 '17 at 07:58
  • Yes, in that case just call the custom version something different like 'remotePostal' – imjosh Mar 18 '17 at 15:53