3
votes

I'm trying to do very basic form validation with Bootstrap 4. For some reason, when adding class 'is-valid' to my 2nd input in my example, because the 1st input has the 'is-invalid' class, the 2nd input will have green borders (as it should since it's valid!), BUT it'll also have the invalid-feedback div showing (the "this field is required" message!).

See:

enter image description here

I'm not sure what I'm doing wrong here... Here's the code:

/**
 * AJAX Post script
 */

const ERROR_TYPE_FATALERROR = 1;
const ERROR_TYPE_INPUTERROR = 2;
const ERROR_TYPE_GLOBALMESSAGE = 3;


// Run through the validate() function ONLY once the form as been submitted once!
// Otherwise, user will get validation right away as he types! Just a visual thing ...
var formSubmittedOnce = false;

/**
 * submitFormData()
 * Serialize and post form data with an AJAX call
 *
 * Example: onClick="submitFormData('frm1',['username','email'])"
 * 
 * @param string formid essentially the 'id' of the container holding all form elements (i.e. <tr id="rowfrm_1">, <form id='frm1'>, etc.)
 * @param array fields list of field names that may produce input errors (i.e. ['username','email'] )
 */
 function submitFormData(formid, fields) {
  
    // flag form was submitted once!
    formSubmittedOnce = true;
  
    // ----------------------------------
    // first rehide all error containers
    // ----------------------------------    
    $('#fatalError').removeClass('d-block');
    $('#fatalErrorID').removeClass('d-block');
    $('#fatalErrorTrace').removeClass('d-block');
    $('#fatalErrorGoBack').removeClass('d-block');
    
    $('#globalMessage').removeClass('d-block');
    $('#globalMessageID').removeClass('d-block');
    $('#globalMessageTrace').removeClass('d-block');
    $('#globalMessageFooter').removeClass('d-block');
    $('#globalMessageMailLink').removeClass('d-block');
    $('#globalMessageGoBackLink').removeClass('d-block');

    // rehide error containers of all inputs that might produce errors 
    if (fields != null){ 
      for (const f of fields) {
        $('#' + f + '_inputError').removeClass('d-block');
      }
    }


    // ----------------------------------
    // loop form elements and validate required fields
    // ----------------------------------
    var formNode = $("#"+formid);
    var formInputs = formNode.find("select, textarea, input");    
    var submit = true;

    for(var i = 0; i < formInputs.length; ++i) {
      var input = formInputs[i];

      // validate fields
      if( validate(input) === false ){
        submit = false;
      }
    } 


    if(submit === true) {

    // ----------------------------------
    // get form data and serialize it!
    // ----------------------------------
  
      // formid comes from a <form>. just serialize it
      if( formNode.prop("tagName") === "FORM" ){
        var formData = formNode.serialize();
      }
      // formid doesn't come from a <form>
      else {
  
        // get all form control
        var myInputs = formNode.clone();
  
        // bug with clone() and SELECT controls: it'll only get the value of the option having the 'selected' attribute (the default value)
        // this hack will change the value clone() got to the actual user selected value, and not the default set value!
        formNode.find('select').each(function(i) {
          myInputs.find('select').eq(i).val($(this).val());
        })
  
        // create a dummy form, append all inputs to it and serialize it.
        var formData = $('<form>').append(myInputs).serialize();
      }


      // ----------------------------------
      // POST !
      // ----------------------------------
      $.ajax({
        type: 'POST',
        url: $(location).attr('href'),
        data: formData,
        dataType : "json",
      }).done(function(response) {
  
        // get response
        if(response) {
    
          // if we got success, redirect if we got a redirect url!
          if ( response.success != null ) {
            if (typeof response.success === "string") {
              window.location.replace(response.success);
            }
          }
          
          // if anything else, PHP returned some errors or a message to display (i.e. 'data saved!')
          else { 
            showMessages(response);
          }
        }
  
        // Successful post, but no response came back !?
        // assume success, since no 'success' response came back, thus keeping same page as is
        else {
          console.warn("Post sent, but no response came back!? Assuming successful post...");
        }
  
      }).fail(function(xhr, status, error) { // we get here if we don't have a proper response/json sent!
        
        console.error("Ajax failed: " + xhr.statusText);
        console.error(status);
        console.error(error);
  
        var ajaxError = {
          'type' : ERROR_TYPE_FATALERROR,
          'message' : '<strong>Ajax failure!</strong><br/><br/>' + status + '<br/><br/>' + error, 
          'trace' : null, 
          'id' : null,
          'goback' : null,
          'adminMailtoLnk' : 'mailto:' + '[email protected]'
        };
  
        showMessages(ajaxError);
        
      });
    }
}


/**
 * showMessages()
 * show error messages in page based on JSON response
 * @param response JSON object holding response with (error) messages to display
 */
function showMessages(response){
  
  // error type
  switch (response.type) {
    
    // ----------------------------
    // GLOBAL MESSAGE
    // ----------------------------
    case ERROR_TYPE_GLOBALMESSAGE:
      $('#globalMessage').addClass('d-block');
    
      // set global message header message type 
      $('#globalMessage').removeClass("error warning info");           
      $('#globalMessage').addClass(response.gmType);

      $('#globalMessageIcon').removeClass("fa-exclamation-triangle fa-info-circle");
      $('#globalMessageIcon').addClass(response.gmIcon);
                      
      $('#globalMessageTitle').empty();
      $('#globalMessageTitle').append(response.gmTitle);
        
      // set message
      $('#globalMessagePH').empty();
      $('#globalMessagePH').append(response.message);
        
      // set uniq error id
      if (response.id != null) {
        $('#globalMessageID').addClass('d-block');
        $('#globalMessageIDPH').empty();
        $('#globalMessageIDPH').append(response.id);
      }
  
      // set stacktrace
      if(response.trace != null) {
      $('#globalMessageTrace').addClass('d-block');
        $('#globalMessageTracePH').empty();
        $('#globalMessageTracePH').append(response.trace);
      }
        
      // set footer
      if( (response.showContactAdmin == true) || (response.goback != null) ) {
         
        $('#globalMessageFooter').addClass('d-block');

        // contact admin
        if(response.showContactAdmin == true) {
          $('#globalMessageMailLink').addClass('d-block');
          $('#globalMessageMailLinkPH').attr('href', response.adminMailtoLnk);
        }

        // go back 
        if(response.goback != null){
          $('#globalMessageGoBackLink').addClass('d-block');
          $('#globalMessageGoBackLinkPH').attr('href', response.goback);
        }

      }

      break;

    // ----------------------------
    // FATAL ERROR
    // ----------------------------              
    case ERROR_TYPE_FATALERROR:

      // hide content if we got a fatal as to prevent user from fiddling around and not reading the message!
      $('#content').addClass('d-none'); 

      $('#fatalError').addClass('d-block');

      // set message
      $('#fatalErrorMessagePH').empty();
      $('#fatalErrorMessagePH').append(response.message);

      // reset mailto link
      $('#fatalErrorMailLink').attr('href', response.adminMailtoLnk);

      // set stacktrace
      if (response.trace != null) {
      $('#fatalErrorTrace').addClass('d-block');
        $('#fatalErrorTracePH').empty();
        $('#fatalErrorTracePH').append(response.trace);
      }

      // set uniq error id
      if (response.id != null) {
        $('#fatalErrorID').addClass('d-block');
        $('#fatalErrorIDPH').empty();
        $('#fatalErrorIDPH').append(response.id);
      }              

      // set 'go back' url
      if(response.goback != null) {
        $('#fatalErrorGoBack').addClass('d-block');
        $('#fatalErrorGoBackLink').attr('href', response.goback);
      }

      break;

    // ----------------------------
    // INPUT ERROR
    // ---------------------------- 
    case ERROR_TYPE_INPUTERROR:

      for (var field in response.fields) {
        var msg = eval('response.fields.' + field);
        $('#' + field + '_inputError').addClass('d-block')
        $('#' + field + '_inputError_message').empty();
        $('#' + field + '_inputError_message').append(msg);
      }

      break;

    default:
      console.error('Got an invalid error type from the response!');

  }

}
 
 /**
  * validate()
  * Validate if field is empty or not
  * @param input form element
  * @return boolean
  */
function validate(input) {

  if(formSubmittedOnce === true) {
    
    if( input.hasAttribute('required') ) {
      if(input.value.trim() === '') {
        input.classList.remove('is-valid');
        input.classList.add('is-invalid');
        return false;
      }
      else {
        input.classList.remove('is-invalid');
        input.classList.add('is-valid');
        return true;
      }
    }
  
    else {
      // if we get here, then any other inputs not marked as 'required' are valid
      input.classList.add('is-valid');
    }

  }
  
}
<html>

<head>
  <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet" type="text/css">
  <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
</script>
</head>

<body>

<form id="testfrm" class="form-group">

Username: <input type="text" name="username" aria-describedby="username_required username_inputError" class="form-control is-invalid" oninput="validate(this)" required/><br>
<div id="username_required" class="pl-1 invalid-feedback">
  This field is required!
</div>

<!-- if bad username format or already taken, print form input error -->
<div id="username_inputError" class="col alert alert-danger alert-dismissible fade show mt-2 py-2 pl-3 pr-5 text-left d-none">
  <small>
    <strong>Error!</strong> <span id="username_inputError_message"></span>
    <button type="button" aria-label="Close" class="close pt-1 pr-2" onclick="$('#username_inputError').removeClass('d-block').addClass('d-none');">×</button>
  </small>
</div>

Email: <input type="text" name="email" aria-describedby="email_required email_inputError" class="form-control is-valid" oninput="validate(this)" required/><br>
<div id="email_required" class="pl-1 invalid-feedback">
  This field is required!
</div>

<!-- if bad email format or already taken, print form input error -->
<div id="email_inputError" class="col alert alert-danger alert-dismissible fade show mt-2 py-2 pl-3 pr-5 text-left d-none">
  <small>
    <strong>Error!</strong> <span id="email_inputError_message"></span>
    <button type="button" aria-label="Close" class="close pt-1 pr-2" onclick="$('#email_inputError').removeClass('d-block').addClass('d-none');">×</button>
  </small>
</div>

Comment: <input type="text" name="comment" class="form-control is-valid"><br>

<input type="button" value="Submit" onclick="submitFormData('testfrm',['username','email'])" class="is-valid">
</form>

<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.bundle.min.js" integrity="sha384-6khuMg9gaYr5AxOqhkVIODVIvm9ynTT5J4V1cfthmT+emCG6yVmEZsRHdxlotUnm" crossorigin="anonymous"></script>



</body>
</html>

If anyone can shed a light on this.. Cheers! Pat

2
Where is the JS ? - Always Helping
Sorry, if you're talking about the bootstrap JS, then it's normally right before my closing body tag. I've edited the snippet to reflect what I have. If on the otherhand you're talking about the submitFormData() JS, it's not needed here for the purpose of figuring out what's wrong. It would be overkill and more confusing to give that over. I just need to figure how / when to place the 'is-(in)valid' classes, etc. - Pat
addClass when when the value coming to submit form is null or empty. Not sure if you will be asnwer without showing us the actual overkill JS function submitFormData - Always Helping
I'll post it... but I should be able to reproduce this just using this snippet html... I dont want / need to test using the submit button... the snippet, as-is, should show the right invalid-feedback as-is, and it doesn't... You understand what I mean ? - Pat
Nope I do not!! - Always Helping

2 Answers

1
votes

The reason its not working is that you are not wrapping your label and input in form-group div

Validation has changed the way it used to be for bootstrap - Which means the is-valid and is invalid does not know where to look so when its not in the form-group div wrapped it applies the is-invalid message to all the matching divs.

I have added label to make it nice instead of using just Email: <input>

If you submit the form now without any values it will display the errors _ the error will disappear as soon as you type into the input.

Run Snippet below to see it woking.

/**
 * AJAX Post script
 */

const ERROR_TYPE_FATALERROR = 1;
const ERROR_TYPE_INPUTERROR = 2;
const ERROR_TYPE_GLOBALMESSAGE = 3;


// Run through the validate() function ONLY once the form as been submitted once!
// Otherwise, user will get validation right away as he types! Just a visual thing ...
var formSubmittedOnce = false;

/**
 * submitFormData()
 * Serialize and post form data with an AJAX call
 *
 * Example: onClick="submitFormData('frm1',['username','email'])"
 * 
 * @param string formid essentially the 'id' of the container holding all form elements (i.e. <tr id="rowfrm_1">, <form id='frm1'>, etc.)
 * @param array fields list of field names that may produce input errors (i.e. ['username','email'] )
 */
function submitFormData(formid, fields) {

  // flag form was submitted once!
  formSubmittedOnce = true;

  // ----------------------------------
  // first rehide all error containers
  // ----------------------------------    
  $('#fatalError').removeClass('d-block');
  $('#fatalErrorID').removeClass('d-block');
  $('#fatalErrorTrace').removeClass('d-block');
  $('#fatalErrorGoBack').removeClass('d-block');

  $('#globalMessage').removeClass('d-block');
  $('#globalMessageID').removeClass('d-block');
  $('#globalMessageTrace').removeClass('d-block');
  $('#globalMessageFooter').removeClass('d-block');
  $('#globalMessageMailLink').removeClass('d-block');
  $('#globalMessageGoBackLink').removeClass('d-block');

  // rehide error containers of all inputs that might produce errors 
  if (fields != null) {
    for (const f of fields) {
      $('#' + f + '_inputError').removeClass('d-block');
    }
  }


  // ----------------------------------
  // loop form elements and validate required fields
  // ----------------------------------
  var formNode = $("#" + formid);
  var formInputs = formNode.find("select, textarea, input");
  var submit = true;

  for (var i = 0; i < formInputs.length; ++i) {
    var input = formInputs[i];

    // validate fields
    if (validate(input) === false) {
      submit = false;
    }
  }


  if (submit === true) {

    // ----------------------------------
    // get form data and serialize it!
    // ----------------------------------

    // formid comes from a <form>. just serialize it
    if (formNode.prop("tagName") === "FORM") {
      var formData = formNode.serialize();
    }
    // formid doesn't come from a <form>
    else {

      // get all form control
      var myInputs = formNode.clone();

      // bug with clone() and SELECT controls: it'll only get the value of the option having the 'selected' attribute (the default value)
      // this hack will change the value clone() got to the actual user selected value, and not the default set value!
      formNode.find('select').each(function(i) {
        myInputs.find('select').eq(i).val($(this).val());
      })

      // create a dummy form, append all inputs to it and serialize it.
      var formData = $('<form>').append(myInputs).serialize();
    }


    // ----------------------------------
    // POST !
    // ----------------------------------
    $.ajax({
      type: 'POST',
      url: $(location).attr('href'),
      data: formData,
      dataType: "json",
    }).done(function(response) {

      // get response
      if (response) {

        // if we got success, redirect if we got a redirect url!
        if (response.success != null) {
          if (typeof response.success === "string") {
            window.location.replace(response.success);
          }
        }

        // if anything else, PHP returned some errors or a message to display (i.e. 'data saved!')
        else {
          showMessages(response);
        }
      }

      // Successful post, but no response came back !?
      // assume success, since no 'success' response came back, thus keeping same page as is
      else {
        console.warn("Post sent, but no response came back!? Assuming successful post...");
      }

    }).fail(function(xhr, status, error) { // we get here if we don't have a proper response/json sent!

      console.error("Ajax failed: " + xhr.statusText);
      console.error(status);
      console.error(error);

      var ajaxError = {
        'type': ERROR_TYPE_FATALERROR,
        'message': '<strong>Ajax failure!</strong><br/><br/>' + status + '<br/><br/>' + error,
        'trace': null,
        'id': null,
        'goback': null,
        'adminMailtoLnk': 'mailto:' + '[email protected]'
      };

      showMessages(ajaxError);

    });
  }
}


/**
 * showMessages()
 * show error messages in page based on JSON response
 * @param response JSON object holding response with (error) messages to display
 */
function showMessages(response) {

  // error type
  switch (response.type) {

    // ----------------------------
    // GLOBAL MESSAGE
    // ----------------------------
    case ERROR_TYPE_GLOBALMESSAGE:
      $('#globalMessage').addClass('d-block');

      // set global message header message type 
      $('#globalMessage').removeClass("error warning info");
      $('#globalMessage').addClass(response.gmType);

      $('#globalMessageIcon').removeClass("fa-exclamation-triangle fa-info-circle");
      $('#globalMessageIcon').addClass(response.gmIcon);

      $('#globalMessageTitle').empty();
      $('#globalMessageTitle').append(response.gmTitle);

      // set message
      $('#globalMessagePH').empty();
      $('#globalMessagePH').append(response.message);

      // set uniq error id
      if (response.id != null) {
        $('#globalMessageID').addClass('d-block');
        $('#globalMessageIDPH').empty();
        $('#globalMessageIDPH').append(response.id);
      }

      // set stacktrace
      if (response.trace != null) {
        $('#globalMessageTrace').addClass('d-block');
        $('#globalMessageTracePH').empty();
        $('#globalMessageTracePH').append(response.trace);
      }

      // set footer
      if ((response.showContactAdmin == true) || (response.goback != null)) {

        $('#globalMessageFooter').addClass('d-block');

        // contact admin
        if (response.showContactAdmin == true) {
          $('#globalMessageMailLink').addClass('d-block');
          $('#globalMessageMailLinkPH').attr('href', response.adminMailtoLnk);
        }

        // go back 
        if (response.goback != null) {
          $('#globalMessageGoBackLink').addClass('d-block');
          $('#globalMessageGoBackLinkPH').attr('href', response.goback);
        }

      }

      break;

      // ----------------------------
      // FATAL ERROR
      // ----------------------------              
    case ERROR_TYPE_FATALERROR:

      // hide content if we got a fatal as to prevent user from fiddling around and not reading the message!
      $('#content').addClass('d-none');

      $('#fatalError').addClass('d-block');

      // set message
      $('#fatalErrorMessagePH').empty();
      $('#fatalErrorMessagePH').append(response.message);

      // reset mailto link
      $('#fatalErrorMailLink').attr('href', response.adminMailtoLnk);

      // set stacktrace
      if (response.trace != null) {
        $('#fatalErrorTrace').addClass('d-block');
        $('#fatalErrorTracePH').empty();
        $('#fatalErrorTracePH').append(response.trace);
      }

      // set uniq error id
      if (response.id != null) {
        $('#fatalErrorID').addClass('d-block');
        $('#fatalErrorIDPH').empty();
        $('#fatalErrorIDPH').append(response.id);
      }

      // set 'go back' url
      if (response.goback != null) {
        $('#fatalErrorGoBack').addClass('d-block');
        $('#fatalErrorGoBackLink').attr('href', response.goback);
      }

      break;

      // ----------------------------
      // INPUT ERROR
      // ---------------------------- 
    case ERROR_TYPE_INPUTERROR:

      for (var field in response.fields) {
        var msg = eval('response.fields.' + field);
        $('#' + field + '_inputError').addClass('d-block')
        $('#' + field + '_inputError_message').empty();
        $('#' + field + '_inputError_message').append(msg);
      }

      break;

    default:
      console.error('Got an invalid error type from the response!');

  }

}

/**
 * validate()
 * Validate if field is empty or not
 * @param input form element
 * @return boolean
 */
function validate(input) {

  if (formSubmittedOnce === true) {

    if (input.hasAttribute('required')) {
      if (input.value.trim() == '') {
        input.classList.remove('is-valid');
        input.classList.add('is-invalid');
        return false;
      } else {
        input.classList.remove('is-invalid');
        input.classList.add('is-valid');
        return true;
      }
    } else {
      // if we get here, then any other inputs not marked as 'required' are valid
      input.classList.add('is-valid');
    }

  }

}
<html>

<head>

  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">

  <!-- jQuery library -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

  <!-- Popper JS -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>

  <!-- Latest compiled JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"></script>
</head>

<body>

  <form id="testfrm" class="form-group">

    <div class="form-group">

      <label class="form-control-label" for="username_required">Username</label>
      <input type="text" name="username" aria-describedby="username_required username_inputError" class="form-control is-invalid" oninput="validate(this)" required/><br>
      <div id="username_required" class="pl-1 invalid-feedback">
        This field is required!
      </div>

      <!-- if bad username format or already taken, print form input error -->
      <div id="username_inputError" class="col alert alert-danger alert-dismissible fade show mt-2 py-2 pl-3 pr-5 text-left d-none">
        <small>
    <strong>Error!</strong> <span id="username_inputError_message"></span>
    <button type="button" aria-label="Close" class="close pt-1 pr-2" onclick="$('#username_inputError').removeClass('d-block').addClass('d-none');">×</button>
  </small>
      </div>

      <div class="form-group">
        <label class="form-control-label" for="email_required">Email</label>

        <input type="text" name="email" aria-describedby="email_required email_inputError" class="form-control is-valid" oninput="validate(this)" required/><br>

        <div id="email_required" class="pl-1 invalid-feedback">
          This field is required!
        </div>
      </div>


      <!-- if bad email format or already taken, print form input error -->
      <div id="email_inputError" class="col alert alert-danger alert-dismissible fade show mt-2 py-2 pl-3 pr-5 text-left d-none">
        <small>
    <strong>Error!</strong> <span id="email_inputError_message"></span>
    <button type="button" aria-label="Close" class="close pt-1 pr-2" onclick="$('#email_inputError').removeClass('d-block').addClass('d-none');">×</button>
  </small>
      </div>

      Comment: <input type="text" name="comment" class="form-control is-valid"><br>

      <input type="button" value="Submit" onclick="submitFormData('testfrm',['username','email'])" class="is-valid">
  </form>





</body>

</html>
0
votes

To make it more clear for others:

<div>
  Username: <input type="text" name="username" aria-describedby="username_required" class="form-control is-invalid" oninput="validate(this)" required/><br>
  <div id="username_required" class="pl-1 invalid-feedback">
    This field is required!
  </div>
</div>

<div>
  Email: <input type="text" name="email" aria-describedby="email_required" class="form-control is-valid" oninput="validate(this)" required/><br>
  <div id="email_required" class="pl-1 invalid-feedback">
    This field is required!
  </div>
</div>

seems to work! Just needed to encapsulate the input with the invalid-feedback ....

Thanks for your help AlwaysHelping!