Why is window[func] undefined in JS? - javascript

I am writing a script that validates form fields. The form input has a data-validate attribute that contains a CSV of different functions. I loop through all inputs containing a data-validate value, separate those values into an array, and loop through that array to call the corresponding function. My problem is that window[func] is undefined - I get "Cannot read property 'apply' of undefined" message. What am I doing wrong? Here's a jsFiddle
<input type="text" name="first_name" data-validate="min_length[4]">
(function($){
var fields = [];
var i = 0;
$('[data-validate]').each(function(){
var rules = $(this).data('validate').split("|");
var label = $(this).parent().find('label').first();
fields[i] = {
name: $(this).attr("name"),
label: label.text(),
rules: rules,
element: $(this)
}
i++;
});
$.each(fields, function(key, field){
field.element.on('focusout', function(){
$.each(field.rules, function(key1, rule){
if(rule.includes("[")){
var rule = rule.match(/(.*?)\[(.*?)\]/);
func = rule[1];
var len = rule[2];
params = [field.element.val(), len];
} else {
func = rule;
params = [field.element.val()];
}
if(callFunc(func, params)){
field.element.addClass('is-valid').removeClass('is-invalid');
} else {
field.element.addClass('is-invalid').removeClass('is-valid');
}
});
});
});
})(jQuery);
function min_length(str, len){
return str.length > len;
}
function callFunc (func, arguments){
window[func].apply(null, Array.prototype.slice.call(arguments, 1));
}

I think I made it work with some improvements, check it out:
(function($){
window.min_length = function(str, len) {
return str.length > len;
}
function callFunc(func, arguments){
return window[func](...arguments);
}
var fields = [];
$('[data-validate]').each(function(){
var rules = $(this).data('validate').split("|");
var label = $(this).parent().find('label').first();
fields.push({
name: $(this).attr("name"),
label: label.text(),
rules: rules,
element: $(this)
});
});
$.each(fields, function(key, field){
field.element.on('input', function(){
$.each(field.rules, function(key1, rule){
if(rule.includes("[")){
var rule = rule.match(/(.*?)\[(.*?)\]/);
func = rule[1];
var len = rule[2];
params = [field.element.val(), len];
} else {
func = rule;
params = [field.element.val()];
}
if(callFunc(func, params)){
field.element.removeClass('is-invalid').addClass('is-valid');
} else {
field.element.removeClass('is-valid').addClass('is-invalid');
}
});
});
});
})(jQuery);
input {
outline: 0;
}
.is-valid {
border: 1px solid green;
}
.is-invalid {
border: 1px solid red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input type="text" name="first_name" data-validate="min_length[4]">
Some issues:
You check the result of function callFunc(func, params) call but it
returns nothing, i.e. undefined, so your condition is always
false. You should return the result of min_length() from
callFunc().
I found 2 classes is-valid in the css section of the snippet on
jsFiddle. One of them should be is-invalid.
Also due to some closure issues it seems that your
function min_height didn't become a property of window. So, I
placed it inside the IIFE and explicitly added it to window which
works because of closures, again.
There is no need for i variable - you can just push() objects
into array.
Also it is possible to use ... operator calling min_length
inside callFunc instead of that apply thing.
Consider using input event instead of focusout.

With regard to the specific error you're getting, I'm not getting that at all from your code. However, here's what I figured out with some testing (and fixed):
1) You need to return the value of the function you call in callFunc otherwise you'll just get undefined. In addition you're not passing in the right number of arguments.
return window[func].apply(null, Array.prototype.slice.call(arguments));
2) You need to convert your len parameter to a number from a string:
return str.length > Number(len);
3) Both of your classes are called is-valid :)
Here's the full working code.

Related

How to pass an object's method as a parameter to another function in Javascript

First take a look at my simple codes below:
function mySecondFunction(objArray,setFunc)
{
for (let i = 0; i < objArray.length; i++)
{
objArray[i].info.setTop(72);
}
}
function myFunction()
{
let myObjArray = [];
for (let i = 0; i < 10; i++)
{
myObjArray.push({
info:{topVar:0,
bottomVar:0,
get top() {return this.topVar;},
get bottom() {return this.bottomVar;},
setTop: function(input) {this.topVar = input;},
setBottom: function(input) {this.bottomVar = input; }
}
});
}
mySecondFunction(myObjArray); // This works Fine
mySecondFunction(myObjArray,setTop); // I want something like this!!!
}
As you can see, I want to pass a method of an object to another function. I know a lot of possible solutions to avoid this, but I want to know whether it is possible or not.
Detach it and pass as an argument. Remember to use call to set the intended this value.
function mySecondFunction(objArray, setFunc)
{
for (let i = 0; i < objArray.length; i++)
{
setFunc.call(objArray[i].info, 72);
/* explicitly telling that:
please set 'this' value in this function to be 'objArray[i].info' when running,
allowing, e.g. `this.topVar` in
`setTop: function(input) {this.topVar = input;}`
to be operating on `objArray[i].info.topVar` */
}
}
function myFunction()
{
let myObjArray = [];
for (let i = 0; i < 10; i++)
{
myObjArray.push({
info:{topVar:0,
bottomVar:0,
get top() {return this.topVar;},
get bottom() {return this.bottomVar;},
setTop: function(input) {this.topVar = input;},
setBottom: function(input) {this.bottomVar = input; }
}
});
}
mySecondFunction(myObjArray, myObjArray[0].info.setTop);
/* once detaching the method from the object,
(if we are not using arrow functions),
we lose 'this' value, meaning we are losing
the target of object that we want to operate on */
console.log(myObjArray)
}
myFunction();
You can target item number in the array list. You can do statically (i.e. 1-???) or dynamically with an iteration and a variable. You can then the object property within that. For example:
myObjArray[0].info.setTop
That will target the 1st item in the array. Be sure to omit parentheses (()) when passing the method as you want to pass the function reference not the result

Why is a function returning undefined and how to debug it?

I'm experimenting with closures and classes in data variables and in the example below I'm getting undefined even though I placed a console.log() right before the function returns the result and it isn't undefined. It seems to work if it isn't attached to an event handler. Can someone tell me why is this happening and if there is a way to spot where exactly does the error happen? When debugging it goes from the console log straight to the error and I don't see how that makes sense.
To trigger the error run the snippet and click on the names.
The same functions in $('#Individuals').data('functions') can be chained and work fine when called in IndividualsList(), but not from the event listener, then the result becomes undefined.
$(document).ready(function() {
var thisWindow = $('#Individuals');
var randomNames = ['Sonia Small', 'Kurt Archer', 'Reese Mullins', 'Vikram Rayner', 'Jethro Kaye', 'Suhail Randolph', 'Kaydon Crouch', 'Jamaal Elliott', 'Herman Atkins', 'Sia Best', 'Kory Gentry', 'Fallon Sawyer', 'Zayyan Hughes', 'Ayomide Byers', 'Emilia Key', 'Jaxson Guerrero', 'Gracey Frazier', 'Millie Mora', 'Akshay Parker', 'Margareta Emiliana'];
var generatedIndividuals = [];
function generateIndividual(name) {
return {
IndividualName: name
};
}
function IndividualsList(element) {
var list = [];
this.add = function(thisIndividual) {
$('#Individuals').data('functions').init(element, list).add(thisIndividual);
}
this.refresh = function() {
$('#Individuals').data('functions').init(element, list).refresh();
}
this.sort = function(order) {
$('#Individuals').data('functions').init(element, list).sort(order);
}
}
thisWindow.data('functions', (function() {
var element = $();
var list = [];
return {
add: function(thisIndividual) {
list.push(thisIndividual);
return thisWindow.data('functions');
},
init: function(thisElement, thisList) {
element = thisElement;
list = thisList;
return thisWindow.data('functions');
},
refresh: function() {
var thisList = element.html('');
for (let i = 0; i < list.length; i++) {
thisList.append(
'<div>' + list[i].IndividualName + '</div>'
);
}
return thisWindow.data('functions');
},
sort: function(order) {
list.sort(function(a, b) {
if (a.IndividualName < b.IndividualName) return -1 * order;
if (a.IndividualName > b.IndividualName) return 1 * order;
return 0;
});
console.log(thisWindow.data('functions'));
return thisWindow.data('functions');
}
}
})());
for (let i = 0; i < 20; i++) {
let nameNum = Math.floor(Math.random() * randomNames.length);
let thisClient = generateIndividual(randomNames[nameNum]);
generatedIndividuals.push(thisClient);
}
(function() {
var targetElement = thisWindow.find('div.individuals-list');
var targetData = {}
targetElement.data('individualsList', new IndividualsList(targetElement));
targetData = targetElement.data('individualsList');
for (let i = 0; i < generatedIndividuals.length; i++) {
targetData.add(generatedIndividuals[i]);
}
targetData.refresh();
})();
thisWindow.on('click', '.individuals-list', function() {
var thisElem = $(this);
var order = parseInt(thisElem.data('order'));
thisWindow.find('div.individuals-list').data('individualsList').sort(order).refresh();
thisElem.data('order', order * (-1));
});
});
.individuals-list {
border: 1px solid;
cursor: pointer;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="Individuals">
<div class="individuals-list" data-order="1"></div>
</div>
https://jsfiddle.net/Kethus/ymgwrLhj/
You are referring to the wrong sort() function, hence call it incorrectly so it returns undefined. Then you call refresh() on undefined that was returned from sort. Here's why:
In your IFFE, you use .data() to set the data = new IndvidualsList on thisWindow.find('div.individuals-list')
This code:
thisWindow.find('div.individuals-list').data('individualsList')
Returns that instantiated IndividualsList Object:
IndividualsList = $1
add: function(thisIndividual)
refresh: function()
sort: function(fieldName, order)
IndividualsList Prototype
Note the sort() function's definition. Sort in this object requires two parameters, fieldName and order; yet you call sort() and only pass order;
This indicates your expectation for the sort() function is incorrect or the wrong sort function is being made available at that line of code (in the click handler).
How to debug
Set a breakpoint at line 132 of the provided JavaScript in the
Fiddle.
Click a name in the list.
While at the breakpoint (execution paused), move to the console and run this in the console:
thisWindow.find('div.individuals-list').data('individualsList')
Note the sort() function definition in the list of functions
Next, in the console run this statement:
thisWindow.find('div.individuals-list').data('individualsList').sort(order)
Note the return is undefined <-- This is the issue
The returned value doesn't transfer from the closure to the instance that called it, the class has to be changed like so:
function IndividualsList(element) {
var list = [];
this.add = function(thisIndividual) {
return $('#Individuals').data('functions').init(element, list).add(thisIndividual);
}
this.refresh = function() {
return $('#Individuals').data('functions').init(element, list).refresh();
}
this.sort = function(order) {
return $('#Individuals').data('functions').init(element, list).sort(order);
}
}
The breakpoint could have been in one of IndividualsList()'s methods so it can be noticed that the closure returns the desired object while the method does not. Different names for either the functions or methods would help to reinforce that they are separate.

Pass dynamic params to IIFE

I've got this issue with passing a variable to an IFFE. did some reading, still didn't figure it out. would really appreciate some guidance here.
i have a click event handler function that gets a certain ID from the
DOM when clicked.
i need to pass that ID to an IIFE
that IFFE needs to either add/remove that ID from an array,
depending if it's already there or not.
This is what I got:
Event:
$(document).on('click', 'input[type="checkbox"]', check);
Click Handler:
function check() {
var id = $(this).closest('ul').attr('data-id');
return id;
}
IIFE:
var checkID = (function (val) {
var arr = [];
return function () {
var i = arr.indexOf(val);
if (i === -1) {
arr.push(val);
} else {
arr.splice(i, 1);
}
return arr;
}
})(id);
right now i'm getting the ID, but returning it to nowhere.
in my IIFE, i did pass an id variable, but it's undefined.
so, how do I pass the ID variable im getting from check() to checkID IIFE?
other solutions are also welcome.
Thanks
In your clickHandler
function check() {
var id = $(this).closest('ul').attr('data-id');
checkID(id);
}
and change checkID to
var checkID = (function () {
var arr = [];
return function (val) {
var i = arr.indexOf(val);
if (i === -1) {
arr.push(val);
} else {
arr.splice(i, 1);
}
return arr;
}
})();
I think you need to do things sort of the other way around. Your check function would return a function used by the event handler, but it would also take a callback to be called after the click handler has run, passing your array.
The check function would look like a mash-up of both your functions:
function check(callback){
var arr = [];
return function(){
var id = $(this).closest('ul').attr('data-id');
var i = arr.indexOf(id);
if (i === -1) {
arr.push(id);
} else {
arr.splice(i, 1);
}
callback(arr);
}
}
As you can see, it takes as a parameter a callback function, which will be called on each execution, passing the current array arr. For example, this is my test callback:
function handler(arr){
alert("Array has " + arr.length + " elements");
}
Finally, your event handler would look like this:
$(document).on('click', 'input[type="checkbox"]', check(handler));
Live example: https://jsfiddle.net/src282d6/
Using getter/setter-like functions in your IIFE function makes it much more organized and readable. Then, use these functions to pass, store, and read data across your IIFE function.
var checkID = (function () {
// your array
var arr = [];
// public
return {
// get
getArray: function(){
return arr;
},
// set value
setArray: function(val) {
var i = arr.indexOf(val);
if (i === -1) {
arr.push(val);
} else {
arr.splice(i, 1);
}
}
}
})();
Use it as follows:
checkID.getArray(); // returns default empty array []
checkID.setArray('car1');
checkID.setArray('car2');
checkID.setArray('car3');
checkID.setArray('car4');
checkID.setArray('car4'); // test splice()
checkID.getArray(); // returns ["car1", "car2", "car3"]

Linking property of a custom element to it's attribute

Question & Demo
I've recently started to work with custom elements.
As you know, a HTMLElement has both a markup inside the document, and a JavaScript object. So, with my custom element, I've tried to link the JavaScript object properties with the element's attributes.
So, if any of those is updated, the other would be updated as well. But this isn't happening and I swear I've tried everything, maybe is something stupid I'm missing but for me, how this code is behaving is a freaking mistery.
After reading the code explanation below and seen the demo, you should be able to understand my question:
Why are the custom element attributes updating correctly, but not it's properties?
I've setup a JSFiddle to illustrate my problem, and I will be going over how the code is supposed to work in this post.
HTML
<e-button color="red" width="250px">RED BUTTON</e-button>
Well it rarely gets any simpler than that. I create a custom object called "e-button", with color=red and width=250px.
JavaScript
var eButtonProto = Object.create(HTMLElement.prototype);
eButtonProto.createdCallback = function() {
this.__htmlToJsProp(); //Gets all the HTML attributes and makes them accessible via JS.
this.__processAttr(); //Makes decision upon predefined attributes.
}
eButtonProto.__htmlToJsProp = function() {
var attr = this.attributes;
for (var i = 0; i < attr.length; i++) {
var current = attr[i];
var name = current.name;
var value = current.value;
this[name] = value;
Object.defineProperty(this, name, {
get: function() {
return this.getAttribute(name);
},
set: function(val) {
this.setAttribute(name, val);
}
});
}
}
eButtonProto.attributeChangedCallback = function(name, oldVal, val) {
this[name] = val;
this.__processAttr();
}
eButtonProto.__processAttr = function() {
var color = this.color || this.defaults.color;
this.style.backgroundColor = color;
}
eButtonProto.defaults = {
color: "whitesmoke"
}
var eButton = document.registerElement("e-button", {
prototype: eButtonProto
});
window.onload = function() {
redButton = document.querySelector("e-button[color=red]");
console.log("button ATTRIBUTES", redButton.getAttribute("color"), redButton.getAttribute("width"));
console.log("button PROPERTIES", redButton.color, redButton.width);
} < /script>
The really important code snippets here are these, which essentialy should make my idea work, first, the __htmlToJsProp() function:
eButtonProto.__htmlToJsProp = function() {
var attr = this.attributes; //Gets the element's attributes.
for (var i = 0; i < attr.length; i++) {
var current = attr[i]; //Element attribute name,value pair.
var name = current.name; //Attribute name.
var value = current.value; //Attribute value.
Object.defineProperty(this, name, { //Defines the element property from the attribute name, for simplicity I will be using the color attribute as my example.
get: function() {
return this.getAttribute(name); //When accessing element.color you should get element.getAttribute("color")
},
set: function(val) {
this.setAttribute(name, val); //When setting element.color = "red" you should also be doing element.setAttribute("color","red");
}
});
this[name] = value; //Sets element.color = "red"
}
}
and then the attributeChangedCallback function:
eButtonProto.attributeChangedCallback = function(name, oldVal, val) {
this[name] = val; //This would be the other way around, if the attribute is updated via setAttribute, or the browser console, the property is updated (works).
this.__processAttr(); //You can ignore this
}
Conclusions
You see after testing A LOT I found that if you place yourself in the for loop and output the property value, it will give you element.color = "red" and element.width = "250px";
But if you test it outside the for loop, it gives you element.color = "250px" and element.width = "250px" for the properties but the attributes update properly, that is element.getAttribute("color") = "red" and element.getAttribute("width") = "250px".
If you made it this far, well thanks, hopefully you can find a way out of this problem, which I really don't seem to be able to solve, happy coding :)
Your issue seems to be within the for loop, the getters and setters are called later, so the value of i isn't what you think it is, the loop completes and sets i to the latest iterated value.
You'll solve it with a closure
eButtonProto.__htmlToJsProp = function () {
var attr = this.attributes;
for (var i = 0; i < attr.length; i++) {
(function(current, self) {
var name = current.name;
var value = current.value;
Object.defineProperty(self, name, {
get: function () {
return this.getAttribute(name);
},
set: function (val) {
this.setAttribute(name, val);
}
});
self[name] = value;
})(attr[i], this);
}
}
FIDDLE

JavaScript: Intercepting form submit and identifying which form made the submission

I am trying to intercept form submits from webpages I dont control.
My current implementation is ...
// During onLoad, loop through all forms and for each form object
var prevonsubmit = formobj.onsubmit;
if (prevonsubmit) {
formobj.onsubmit = function f() {
if(prevonsubmit()) {
interceptform();
return true;
}
return false;
};
} else {
formobj.onsubmit = function ff() {
interceptform();
return true;
};
}
The problem with this is, inside interceptform(), I am unable to identify which form actually made this submission. Is there a way I actually get the form object that is trying to submit? Keep in mind that some of the forms I see do not have a name or id specified and there is more than one form (in the same webpage) with same action.
Edit:
The purpose is capture the content in the input tags that belong to the form.
A made up example of what I see in a form:
<form action="https://duckduckgo.com/html/" method="GET">
<input type="text" name="q"/>
</form>
<form action="https://duckduckgo.com/html/" method="GET">
<input type="text" name="l"/>
</form>
<form action="https://duckduckgo.com/html/" method="GET">
<input type="text" name="l"/>
<input type="text" name="q"/>
</form>
Edit2:
Based on #ruakh answer, the solution I ended up using:
var prevonsubmit = formobj.onsubmit;
if (prevonsubmit)
formobj.onsubmit = createOnSubmitFunctionWithOld(prevonsubmit, formobj);
else
formobj.onsubmit = createOnSubmitFunction(formobj);
// Definition of the functions:
function createOnSubmitFunctionWithOld(prevonsubmit,formObj) {
return function () {
if (prevonsubmit()) {
interceptform(formObj);
return true;
}
return false;
};
}
function createOnSubmitFunction(formObj) {
return function () {
interceptform(formObj);
return true;
};
}
You can simply pass formobj as an argument to interceptform():
interceptform(formobj);
But bear in mind that both with formobj and with prevonsubmit, you have to be careful to avoid capturing a variable you don't want to. For example, in JavaScript, this:
var functions = [];
for(var i = 0; i < 10; ++i)
{ functions[i] = function() { return i; }; }
creates ten functions that all return 10, because they all capture the same i variable that's been incremented up to 10 by the time the functions are ever called. In the above example, you could write something like:
function new_constant_function(return_value)
{ return function() { return return_value; }; }
// ...
var functions = [];
for(var i = 0; i < 10; ++i)
{ functions[i] = new_constant_function(i); }
to copy each value of i into a new local variable return_value whose value never changes; or, a bit more tersely, this:
var functions = [];
for(var i = 0; i < 10; ++i)
{ functions[i] = (function(_i){ return function(){ return _i; } })(i); }
You'll almost certainly need to do something similar in your case. (But without seeing a bit more of the context of your code, it's hard to say exactly what.)
Why not loop through document.forms and append a hidden field to each one, or assign an id to the form itself.
If you are loopoing over the forms from a function, then:
> var prevonsubmit = formobj.onsubmit;
> if (prevonsubmit) {
> formobj.onsubmit = function f() {
> if(prevonsubmit()) {
prevonsubmit has a closure to the outer variable, it will reference the last value of prevonsubmit, so they will all reference the same function. This may "work", but will fail if any of your forms has a current listener that is different to any other form.
> interceptform();
You can simply pass this from the function to interceptform:
interceptform(this);
and interceptform will be passed a reference to the element calling the function (presumably the form being submitted).
> return true;
> }
> return false;
That will (probably) cancel submission of any form that doesn't have an existing listener. Is that what you want?
> };
> } else {
> formobj.onsubmit = function ff() {
Named function expressions are known to be buggy in at least one widely used browser. If you don't need a name (and it doesn't seem to be used here for anything), dont' use one. Keep the function anonymous.
> interceptform();
> return true;
> };
> }

Categories

Resources