0

I am writing some logic to handle the behavior of input elements when a user focuses in on it, then clicks out of it and tries to submit without typing anything out in the input. An error should be the result. Now when the user focuses back into the input field, the error should remain until the uses starts typing something in the input, anything.

Now that is still a work in progress, but what I don't understand or what my expectation was, that I could write this logic out once and it would apply to all input elements, but it doesn't, it seems to just apply it to the first input element. Do I need to copy and paste the logic more than once per element?

I am already violating DRY principles all over the place. Why is this logic not applying to all input elements?

document.querySelector('input').addEventListener('blur', function (e) {
  if (e.target.value.match(/^\s*$/)) {
    document.querySelector('.animated-label').classList.add('error');
    document.querySelector('.error-block').style.display = "block";
  }
});

document.querySelector('input').addEventListener('focus', function (e) {
  if (!e.target.value.match(/^\s*$/)) {
    document.querySelector('.animated-label').classList.remove('error');
    document.querySelector('.animated-label').classList.add('focus');
    document.querySelector('.error-block').style.display = "none";
  }
});
.rds-form .animated-label {
  border: 2px solid #ccc;
  border-radius: 2px;
  color: #767676;
  display: block;
  min-height: 40px;
  position: relative;
}

.rds-form .animated-label.focus {
  border: 2px solid #8b0;
  outline: thin dotted black;
  outline-offset: 2px;
}

.rds-form .animated-label.required.field label:after {
  background-color: #8b0;
  border-radius: 50% 50%;
  content: '';
  display: inline-block;
  height: 5px;
  position: relative;
  right: -0.5em;
  top: -0.5em;
  width: 5px;
}

.rds-form .field.animated-label.required.error label:after {
  background-color: #cc2233;
}

.rds-form .field-group.error-group .error-block {
  border-radius: 2px;
  margin-bottom: 30px;
}

.rds-form .field-group.error-group .error-block .help-block {
  padding-left: 40px;
  position: relative;
}
<form class="rds-form">
   <div class="row">
    <div class="col-md-12">
      <div class="field animated-label text required">
        <label class="control-label" for="oldPassword">Current password</label>
        <input class="form-control" type="password" id="oldPassword" aria-required="true" autocomplete="on" style="max-height: 67px; padding-top: 27px;" aria-invalid="true" aria-describedby="error-current-password">
        <div class="error-block" style="display: none;">
          <p class="help-block error" id="error-current-password">This is where the error should be.</p>
        </div>
      </div>
      
      <div class="field animated-label text required">
        <label class="control-label" for="NewPassword_NewPassword">New password</label>
        <input class="form-control" type="password" name="NewPassword.NewPassword" id="NewPassword" aria-required="true" autocomplete="on" style="max-height: 67px; padding-top: 27px;" aria-invalid="true" aria-describedby="error-current-password">
        <div class="error-block" style="display: none;">
          <p class="help-block error" id="error-current-password">This is where the error should be.</p>
        </div>
      </div>
    </div>
  </div>
</form>

So the behavior you are seeing in the first input element, I want it to be the same behavior for all input elements in the form I create.

I tried iterating through the input elements but I am not getting the functionality I was before:

const inputBlurs = document.querySelectorAll('input').addEventListener('blur', function (e) {
  for (const i = 0, inputBlur; inputBlur = inputBlurs[i++];) {
    if (e.target.value.match(/^\s*$/)) {
    document.querySelectorAll('.animated-label').classList.add('error');
    document.querySelectorAll('.error-block').style.display = "block";
  }
}
});

const inputClicks = document.querySelectorAll('input').addEventListener('focus', function (e) {
  for (const i = 0, inputClick; inputClick = inputClicks[i++];) {
    if (!e.target.value.match(/^\s*$/)) {
    document.querySelectorAll('.animated-label').classList.remove('error');
    document.querySelectorAll('.animated-label').classList.add('focus');
    document.querySelectorAll('.error-block').style.display = "none";
  }
}
});

With the below implementation, I am still only getting one input element reacting to the focus and blur:

const errorHandler = e => {
  const eTarg = e.target;
  console.log(eTarg.tagName, e.type)
  const inputs = document.querySelectorAll('input');
  for (const i = 0; i < inputs.length; inputs++) {
    if (eTarg.tagName === "INPUT" && eTarg.value.match(/^\s*$/)) {
      if (e.type === "blur") {
        document.querySelector('.animated-label').classList.add('error');
        document.querySelector('.error-block').style.display = "block";
      } else if (e.type==="focus") {
        document.querySelector('.animated-label').classList.remove('error');
        document.querySelector('.animated-label').classList.add('focus');
        document.querySelector('.error-block').style.display = "none";
      }
    }
  }  
};
const transmit = e => alert(e.target)

document.querySelector('#myForm').addEventListener('focus', errorHandler, true); // NOTE the true
document.querySelector('#myForm').addEventListener('blur', errorHandler, true)
Daniel
  • 14,004
  • 16
  • 96
  • 156
  • Because you are only adding the event listener to the first input element. `querySelector` returns just one element. – Taplar Aug 24 '20 at 15:27
  • If you want the logic on all the inputs, you will have to use `querySelectorAll`, loop over them, and put the event listener on all of them. Or, change up the logic to be in a delegate form. – Taplar Aug 24 '20 at 15:27
  • Hmm, I would still be duplicating code it seems, which is not a priority right now whether I am or not, but from what you shared, I would have to do a `for` loop to handle `blur` and then another `for` loop to handle `focus`. – Daniel Aug 24 '20 at 15:37
  • If the concern is duplicating code, then either use a delegate form of event listening, or extract those anonymous functions out to named functions, and reuse them for your binding loops. – Taplar Aug 24 '20 at 15:38
  • @Taplar, can you review what I added in OP? I just tried to iterate over the `input` elements, but it does not seem to be working. – Daniel Aug 24 '20 at 15:44
  • You're looping in the wrong part. `[...document.querySelectorAll('input')].forEach(input => ...stuff... )` or `var inputs = document.querySelectorAll('input'); for (var i = 0; i < inputs.length; ...` – Taplar Aug 24 '20 at 15:46
  • `querySelectorAll` does not have `addEventListener` and the error message in your console should say that. You need to loop over the HTML collection and bind the event to each element – epascarello Aug 24 '20 at 15:47
  • @Taplar, I implemented the above with a `for` loop but the behavior is still only applying to one input element. – Daniel Aug 24 '20 at 16:18
  • Your loop makes no sense though. You are looping over the return value of `addEventListener` (in an absolutely strange way too) and not over the elements in the node list that `querySelectorAll` returns. – CherryDT Aug 24 '20 at 17:59
  • @CherryDT, yes I realized that afterwards. – Daniel Aug 24 '20 at 18:23

1 Answers1

0

Delegate to the nearest common container

Note focus and blur need the event capturing boolean set to true

const errHandler = e => {
  const tgt = e.target;
  console.log(tgt.tagName, tgt.name, e.type)
  if (tgt.tagName === "INPUT" && tgt.value.match(/^\s*$/)) {
    const parent = tgt.closest(".animated-label");
    const err = parent.querySelector('.error-block');
    if (e.type === "blur") {
      parent.classList.add('error');
      err.querySelector("p.error").textContent = "Please fill "+tgt.name
      err.style.display = "block";
    } else if (e.type==="focus") {
      parent.classList.remove('error');
      parent.classList.add('focus');
      err.style.display = "none";
    }
  }
};
document.querySelector('#myForm').addEventListener('focus', errHandler, true); // NOTE the true
document.querySelector('#myForm').addEventListener('blur', errHandler, true)
.rds-form .animated-label {
  border: 2px solid #ccc;
  border-radius: 2px;
  color: #767676;
  display: block;
  min-height: 40px;
  position: relative;
}

.rds-form .animated-label.focus {
  border: 2px solid #8b0;
  outline: thin dotted black;
  outline-offset: 2px;
}

.rds-form .animated-label.required.field label:after {
  background-color: #8b0;
  border-radius: 50% 50%;
  content: '';
  display: inline-block;
  height: 5px;
  position: relative;
  right: -0.5em;
  top: -0.5em;
  width: 5px;
}

.rds-form .field.animated-label.required.error label:after {
  background-color: #cc2233;
}

.rds-form .field-group.error-group .error-block {
  border-radius: 2px;
  margin-bottom: 30px;
}

.rds-form .field-group.error-group .error-block .help-block {
  padding-left: 40px;
  position: relative;
}
<form class="rds-form" id="myForm">
  <div class="row">
    <div class="col-md-12">
      <div class="field animated-label text required">
        <label class="control-label" for="oldPassword">Current password</label>
        <input class="form-control" type="password" id="oldPassword" name="oldPassword" aria-required="true" autocomplete="on" style="max-height: 67px; padding-top: 27px;" aria-invalid="true" aria-describedby="error-current-password">
        <div class="error-block" style="display: none;">
          <p class="help-block error" id="error-current-password">This is where the error should be.</p>
        </div>
      </div>

      <div class="field animated-label text required">
        <label class="control-label" for="NewPassword_NewPassword">New password</label>
        <input class="form-control" type="password" name="NewPassword.NewPassword" id="NewPassword" aria-required="true" autocomplete="on" style="max-height: 67px; padding-top: 27px;" aria-invalid="true" aria-describedby="error-current-password">
        <div class="error-block" style="display: none;">
          <p class="help-block error" id="error-current-password">This is where the error should be.</p>
        </div>
      </div>
      <div class="field animated-label text required">
        <label class="control-label" for="email">Email</label>
        <input class="form-control" type="text" name="email" id="email" aria-required="true" autocomplete="on" style="max-height: 67px; padding-top: 27px;" aria-invalid="true" aria-describedby="error-email">
        <div class="error-block" style="display: none;">
          <p class="help-block error" id="error-email">This is where the error should be.</p>
        </div>
      </div>
    </div>
  </div>
</form>
mplungjan
  • 169,008
  • 28
  • 173
  • 236
  • this works, but so did my implementation. The question being how to get this work on all the input elements. I have a `for` loop developed but the logic needs to be refactored and thats where I am getting stuck on. – Daniel Aug 24 '20 at 16:51
  • without a `for` loop? I asked because I tried your implementation and it still only works on the first input element. – Daniel Aug 24 '20 at 16:58
  • Exactly.......... – mplungjan Aug 24 '20 at 16:59
  • yours only works on the first input element just like in my environment. The idea is if the user clicks inside any of the input elements and then clicks out, it generates the error block for that particular element. Right now yours is the same as what I have, where only the first input element has this behavior. – Daniel Aug 24 '20 at 17:01
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/220374/discussion-between-daniel-and-mplungjan). – Daniel Aug 24 '20 at 17:23
  • I added handling of the nearest error block. It will work for all "INPUT" currently – mplungjan Aug 24 '20 at 17:55
  • yes that works. – Daniel Aug 24 '20 at 17:57
  • So it works in Codepen but not my project, or rather it only half works in my project. – Daniel Aug 24 '20 at 18:23
  • I can only help if you show the code. Any console errors? – mplungjan Aug 24 '20 at 18:24
  • it works in Firefox but its not working in Chrome, any ideas? – Daniel Aug 24 '20 at 20:44
  • That is not very helpful. Any console errors? – mplungjan Aug 25 '20 at 03:54