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.
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.
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.
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.
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.
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.
checkText(pattern)
captures the correct valueWe 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
.
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.
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>