Search code examples
javascriptiife

Is there a better way to do this than using an IIFE?


I am trying to create a template for forms that checks for a data-regex custom param on an HTML element and then creates a regular expression from it to be used to validate the input. Here is what I have so far.

var textInputs = document.getElementsByClassName("form__text");
for (var i = 0; i < textInputs.length; ++i) {
  if (textInputs[i].getElementsByTagName("input")[0].type == "email") {
    (function() {
      var pattern = /.*[a-zA-Z0-9]+.*@.*\.[a-zA-Z]+/;
      textInputs[i]
        .getElementsByTagName("input")[0]
        .addEventListener("input", function(e) {
          checkText(pattern, e);
        });
    })();
  } else if (textInputs[i].getElementsByTagName("input")[0].type == "text") {
    (function() {
      var patternStr = textInputs[i].getAttribute("data-regex");
      var pattern = patternStr ? new RegExp(patternStr) : null;
      textInputs[i]
        .getElementsByTagName("input")[0]
        .addEventListener("input", function(e) {
          checkText(pattern, e);
        });
    })();  
  }
}

function checkText(pattern, e) {
  if (pattern && e.target.value.search(pattern) == -1) {
    e.target.parentElement.classList.add("form__text--error");
  } else {
    e.target.parentElement.classList.remove("form__text--error");
  }
}
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.form {
  margin: 10px;
}
.form .form__text {
  position: relative;
  margin: 2rem 0 4rem 0;
  display: block;
}
.form .form__text__label {
  position: absolute;
  font-size: 1.4rem;
  padding: 10px;
  opacity: 0.5;
  top: 50%;
  left: 0;
  pointer-events: none;
  transition: all 0.2s ease-out;
  transform: translateY(-50%);
}
.form .form__text__error-label {
  color: red;
  opacity: 0;
  transition: all 0.2s ease-out;
  position: absolute;
  top: 110%;
  left: 0;
}
.form .form__text input[type=text], .form .form__text input[type=email] {
  padding: 10px;
  width: 100%;
  border: 1px solid #ccc;
  border-radius: 5px;
  transition: all 0.2s ease-out;
}
.form .form__text input[type=text]:focus ~ .form__text__label, .form .form__text input[type=text]:not(:placeholder-shown) ~ .form__text__label, .form .form__text input[type=email]:focus ~ .form__text__label, .form .form__text input[type=email]:not(:placeholder-shown) ~ .form__text__label {
  transform: translateX(-15px) translateY(-125%) scale(0.75);
  opacity: 1;
}
.form .form__text input[type=text]:focus, .form .form__text input[type=email]:focus {
  outline: none;
  background: rgba(122, 217, 255, 0.075);
}
.form .form__text--error .form__text__label {
  color: red;
}
.form .form__text--error .form__text__error-label {
  opacity: 1;
}
.form .form__text--error input[type=text], .form .form__text--error input[type=email] {
  border: 1px solid red;
}
.form .form__text--error input[type=text]:focus, .form .form__text--error input[type=email]:focus {
  background: rgba(255, 0, 0, 0.05);
}
<form class="form">
  <label class="form__text">
    <input type="email" id="email" name="email" placeholder=" " />
    <span class="form__text__label">Email</span>
    <span class="form__text__error-label">Invalid Email</label>
  </label>
  <label class="form__text" data-regex="[a-zA-z ]{4,}">
    <input type="text" id="name" name="name" placeholder=" " />
    <span class="form__text__label">Name</span>
    <span class="form__text__error-label">Invalid Name</span>
  </label>
  <label class="form__text">
    <input type="text" id="random" name="random" placeholder=" " />
    <span class="form__text__label">Random Fact</span>
  </label>
</form>

I had to wrap the two addEventListener blocks inside IIFE's because if not, the pattern variable would be overwritten whenever the callback was triggered. I'm curious if there's a cleaner way to do this. I'm assuming that there is some expense to creating the regex objects, which is why I was trying to create them once outside of the callbacks. Thanks in advance.


Solution

  • The problem is that pattern will be a shared variable name for the entire function. Then later when the event listener fires, it will pick up whatever is the last version of pattern rather than the current one. It's a classic problem.

    ES6 solution

    With the introduction of let and const you can have variables in block scope, so the solution is dead simple - change any var to either let or const.

    const textInputs = document.getElementsByClassName("form__text");
    for (let i = 0; i < textInputs.length; ++i) {
      if (textInputs[i].getElementsByTagName("input")[0].type == "email") {
        const pattern = /.*[a-zA-Z0-9]+.*@.*\.[a-zA-Z]+/;
        textInputs[i]
          .getElementsByTagName("input")[0]
          .addEventListener("input", function(e) {
            checkText(pattern, e);
          });
      } else if (textInputs[i].getElementsByTagName("input")[0].type == "text") {
        const patternStr = textInputs[i].getAttribute("data-regex");
        const pattern = patternStr ? new RegExp(patternStr) : null;
        textInputs[i]
          .getElementsByTagName("input")[0]
          .addEventListener("input", function(e) {
            checkText(pattern, e);
          });
      }
    }
    
    function checkText(pattern, e) {
      if (pattern && e.target.value.search(pattern) == -1) {
        e.target.parentElement.classList.add("form__text--error");
      } else {
        e.target.parentElement.classList.remove("form__text--error");
      }
    }
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    .form {
      margin: 10px;
    }
    .form .form__text {
      position: relative;
      margin: 2rem 0 4rem 0;
      display: block;
    }
    .form .form__text__label {
      position: absolute;
      font-size: 1.4rem;
      padding: 10px;
      opacity: 0.5;
      top: 50%;
      left: 0;
      pointer-events: none;
      transition: all 0.2s ease-out;
      transform: translateY(-50%);
    }
    .form .form__text__error-label {
      color: red;
      opacity: 0;
      transition: all 0.2s ease-out;
      position: absolute;
      top: 110%;
      left: 0;
    }
    .form .form__text input[type=text], .form .form__text input[type=email] {
      padding: 10px;
      width: 100%;
      border: 1px solid #ccc;
      border-radius: 5px;
      transition: all 0.2s ease-out;
    }
    .form .form__text input[type=text]:focus ~ .form__text__label, .form .form__text input[type=text]:not(:placeholder-shown) ~ .form__text__label, .form .form__text input[type=email]:focus ~ .form__text__label, .form .form__text input[type=email]:not(:placeholder-shown) ~ .form__text__label {
      transform: translateX(-15px) translateY(-125%) scale(0.75);
      opacity: 1;
    }
    .form .form__text input[type=text]:focus, .form .form__text input[type=email]:focus {
      outline: none;
      background: rgba(122, 217, 255, 0.075);
    }
    .form .form__text--error .form__text__label {
      color: red;
    }
    .form .form__text--error .form__text__error-label {
      opacity: 1;
    }
    .form .form__text--error input[type=text], .form .form__text--error input[type=email] {
      border: 1px solid red;
    }
    .form .form__text--error input[type=text]:focus, .form .form__text--error input[type=email]:focus {
      background: rgba(255, 0, 0, 0.05);
    }
    <form class="form">
      <label class="form__text">
        <input type="email" id="email" name="email" placeholder=" " />
        <span class="form__text__label">Email</span>
        <span class="form__text__error-label">Invalid Email</label>
      </label>
      <label class="form__text" data-regex="[a-zA-z ]{4,}">
        <input type="text" id="name" name="name" placeholder=" " />
        <span class="form__text__label">Name</span>
        <span class="form__text__error-label">Invalid Name</span>
      </label>
      <label class="form__text">
        <input type="text" id="random" name="random" placeholder=" " />
        <span class="form__text__label">Random Fact</span>
      </label>
    </form>

    You can use Babel to transpile your code from ES6+ to ES5, so you'd write the above but it will be automatically tranfformed to older code that works the same as the new one.

    NOTE: let and const will be accepted by IE11 however, they will behave exactly the same as var. IE11 only makes them allowed syntax but as aliases to var. They wouldn't be block scoped.

    ES5 solution

    If you have to use ES5 then it's not that big of a problem. The trouble is lack of block scopes, there are only global and functional scopes. So, you'd need to capture the variable inside a functional scope. An IIFE does that but it looks a bit ugly.

    Curry checkText()

    We can instead make checkText a higher order function that is curried - instead of taking two parameters, it takes one parameter first, then returns a function that takes a second parameter. This looks like this:

    function checkText(pattern){
      return function(e) {
        if (pattern && e.target.value.search(pattern) == -1) {
          e.target.parentElement.classList.add("form__text--error");
        } else {
          e.target.parentElement.classList.remove("form__text--error");
        }
      }
    }
    

    It is a powerful construct here, since it allows us to immediately capture a variable if we call var returnedFunction = checkText(pattern) - now even if the variable pattern changes in the enclosing context, returnedFunction will still hold the previous one.

    Replace the old call

    As a bonus, this allows us to eliminate some useless code. Let's walk step by step for complete understanding - first this

    .addEventListener("input", function(e) {
      checkText(pattern, e);
    });
    

    has to turn into

    .addEventListener("input", function(e) {
      var returnedFunction = checkText(pattern);
      returnedFunction(e);
    });
    

    because we have to pass the parameters into two different functions now. There is still the same problem as before at this point. Right now this doesn't solve anything but I want to show the intervening step. Same issue - when the listener fires, then checkText(pattern) will be executed at which point pattern is already changed.

    Make sure checkText(pattern) captures the correct value

    We need to make sure it fires before being initialised in the event listener:

    var pattern = patternStr ? new RegExp(patternStr) : null;
    var returnedFunction = checkText(pattern);
    /* ...code... */
    .addEventListener("input", function(e) {
      returnedFunction(e);
    });
    

    When we place it at the same level as the pattern variable, so just outside the .addEventListener callback, it works as intended without using an IIFE. We would capture the current pattern variable and changes to it will not affect returnedFunction.

    Simplifying the abstraction

    However, the callback function now is this function(e) { returnedFunction(e); } - a function that takes a parameter and calls a function that passing the same parameter as an argument. Calling <anonymous function>(e) is the same as calling returneFunction(e). The outer wrapper function and the inner returneFunction have the exact same signature and the semantics of calling them with a single argument are virtually the same. Thus, the wrapper is useless now. We can remove it and perform what lambda calculus calls Eta reduction to simplify the abstraction, thus the whole callback becomes

    var pattern = patternStr ? new RegExp(patternStr) : null;
    var returnedFunction = checkText(pattern);
    /* ...code... */
    .addEventListener("input", returnedFunction);
    

    Now the event listener is just returnedFunction. When it is fired, it will be passed the same argument as before.

    Remove the extra variable

    Finally, the last step in the simplification is to just inline the checkText(pattern) call. We don't really need the extra variable here. It's easy to just get rid of it and have

    .addEventListener("input", checkText(pattern));
    

    Done. This was detailed because I wanted to show the process. In practice, it's just converting the checkText() function to a curried variant and then replacing the callback with it. Hopefully, the steps turn it from some weird instructions that happen to work into understanding why that's being done.

    The final result is this:

    var textInputs = document.getElementsByClassName("form__text");
    for (var i = 0; i < textInputs.length; ++i) {
      if (textInputs[i].getElementsByTagName("input")[0].type == "email") {
        var pattern = /.*[a-zA-Z0-9]+.*@.*\.[a-zA-Z]+/;
        textInputs[i]
          .getElementsByTagName("input")[0]
          .addEventListener("input", checkText(pattern));
      } else if (textInputs[i].getElementsByTagName("input")[0].type == "text") {
        var patternStr = textInputs[i].getAttribute("data-regex");
        var pattern = patternStr ? new RegExp(patternStr) : null;
        textInputs[i]
          .getElementsByTagName("input")[0]
          .addEventListener("input", checkText(pattern));
      }
    }
    
    function checkText(pattern){
      return function(e) {
        if (pattern && e.target.value.search(pattern) == -1) {
          e.target.parentElement.classList.add("form__text--error");
        } else {
          e.target.parentElement.classList.remove("form__text--error");
        }
      }
    }
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    .form {
      margin: 10px;
    }
    .form .form__text {
      position: relative;
      margin: 2rem 0 4rem 0;
      display: block;
    }
    .form .form__text__label {
      position: absolute;
      font-size: 1.4rem;
      padding: 10px;
      opacity: 0.5;
      top: 50%;
      left: 0;
      pointer-events: none;
      transition: all 0.2s ease-out;
      transform: translateY(-50%);
    }
    .form .form__text__error-label {
      color: red;
      opacity: 0;
      transition: all 0.2s ease-out;
      position: absolute;
      top: 110%;
      left: 0;
    }
    .form .form__text input[type=text], .form .form__text input[type=email] {
      padding: 10px;
      width: 100%;
      border: 1px solid #ccc;
      border-radius: 5px;
      transition: all 0.2s ease-out;
    }
    .form .form__text input[type=text]:focus ~ .form__text__label, .form .form__text input[type=text]:not(:placeholder-shown) ~ .form__text__label, .form .form__text input[type=email]:focus ~ .form__text__label, .form .form__text input[type=email]:not(:placeholder-shown) ~ .form__text__label {
      transform: translateX(-15px) translateY(-125%) scale(0.75);
      opacity: 1;
    }
    .form .form__text input[type=text]:focus, .form .form__text input[type=email]:focus {
      outline: none;
      background: rgba(122, 217, 255, 0.075);
    }
    .form .form__text--error .form__text__label {
      color: red;
    }
    .form .form__text--error .form__text__error-label {
      opacity: 1;
    }
    .form .form__text--error input[type=text], .form .form__text--error input[type=email] {
      border: 1px solid red;
    }
    .form .form__text--error input[type=text]:focus, .form .form__text--error input[type=email]:focus {
      background: rgba(255, 0, 0, 0.05);
    }
    <form class="form">
      <label class="form__text">
        <input type="email" id="email" name="email" placeholder=" " />
        <span class="form__text__label">Email</span>
        <span class="form__text__error-label">Invalid Email</label>
      </label>
      <label class="form__text" data-regex="[a-zA-z ]{4,}">
        <input type="text" id="name" name="name" placeholder=" " />
        <span class="form__text__label">Name</span>
        <span class="form__text__error-label">Invalid Name</span>
      </label>
      <label class="form__text">
        <input type="text" id="random" name="random" placeholder=" " />
        <span class="form__text__label">Random Fact</span>
      </label>
    </form>