Binding dynamic elements to a function; left with only one bind - javascript

My config (as an example) is setup like so:
this.config = [
{
element: '#Amount',
type: "money",
notNull: true,
error: "You must specify an amount"
},
{
element: '#Type',
type: "string",
notNull: true,
error: "You must specify whether you want a 6 week loan or a 12 month loan"
}
]
I have a bind function that should bind a validation function to each element in the list:
this.bind = function () {
for (var i = 0; i < _this.config.length; i++) {
var arr = _this.config[i];
console.log('Binding ' + arr.element + ' to single input validation')
// bind single validation to each element
$(document).on('keyup', arr.element, function () {
_this.validate(arr.element)
})
}
}
And in the console I am presented with:
Binding #Amount to single input validation
Binding #Type to single input validation
Binding #LoanPurpose to single input validation
The config actually consists of 47 elements, however I am certain that only the last bind remains after iterating through the config so it's as if it's replacing the previous bind each time.
Any pointers would be greatly appreciated
Thanks

This is a classic javascript error. Your nested keyup handler function references a variable arr, which is getting overwritten by the for loop every iteration. So when the keyup handler finally does execute, it's using the arr that is referencing the last element in this.config array.
This link explains why it's bad to make functions within loops. And provides a solution for it.
Here's how your code should probably look:
this.bind = function () {
for (var i = 0; i < _this.config.length; i++) {
var arr = _this.config[i];
console.log('Binding ' + arr.element + ' to single input validation')
// bind single validation to each element
_this.makeBind(arr.element)
}
}
this.makeBind = function (el) {
$(document).on('blur', el, function () {
_this.validate(el)
})
}

config.forEach(function(item) {
$(document.body).on('keyup', item.element, function() {
validate(item, this.value)
});
});
You can do something like the preceding to pass both the item, and the input's current value to the validate function

Related

Assigning of value in a for loop on a mouse event

Why do I always get the last value assigned to the variable
even though I already enclosed it in a function?
When the event mouse up is triggered and getGoogleFiles is called, the last value assigned to resourceId is called. I don't get it.
for ( var i in arrayObj) {
var resourceId = arrayObj[i].ResourceId;
entity_list.onmouseup = function(event) {
parent.changeButtonState(this, event);
(function(resourceId) {
getGoogleFiles(resourceId);
})(resourceId);
}
}
Note: This is different to other JavaScript questions because the onmouseup is not triggered
I followed the creating of another function mentioned here:
JavaScript closure inside loops – simple practical example
for ( var i in arrayObj) {
entity_list.onmouseup = function(event) {
parent.changeButtonState(this, event);
testing(arrayObj[i].ResourceId);
}
}
function testing(index){
return function() { getGoogleFiles(index); };
}
But when the element of "entity_list" is triggered, nothing happens.
I can't use let because the specific browser that I'm using returns a SyntaxError
SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode
Thank you!
You need to use testing() to create the listener function, not something you call inside it.
for (var i in arrayObj) {
entity_list.onmouseup = testing(arrayObj[i].ResourceId, parent);
}
function testing(index, parent) {
return function(event) {
parent.changeButtonState(this, event);
getGoogleFiles(index);
};
}
But you wouldn't have to go through any of this if you use forEach() instead of a for loop, since it creates a new scope for obj in each iteration.
arrayObj.forEach(function(obj) {
entity_list.onmouseup = function(event) {
parent.changeButtonState(this, event);
testing(obj.ResourceId);
}
});
You can't use a var scoped variable here.
But you could assign the resourceId to a data attribute on the relative html element, so you can read it when the event fires.
var arrayObj = [{ResourceId: "test1"}, {ResourceId: "test2"}, {ResourceId: "test3"}];
var entity_list = document.getElementsByClassName("entity_list");
for ( var i in arrayObj) {
entity_list[i].dataset.resourceId = arrayObj[i].ResourceId;
entity_list[i].onmouseup = function(event) {
getGoogleFiles(this.dataset.resourceId);
}
}
function getGoogleFiles(resourceId) {
console.log(resourceId);
}
<span class="entity_list">entity list (item 1)</span>
<span class="entity_list">entity list (item 2)</span>
<span class="entity_list">entity list (item 3)</span>

How do I bake unique accessible/passable values into multiple displayed buttons?

Backstory:
• Varying dynamic items (buttons) will be generated and displayed in a single div.
• Each button is created from a unique object with a unique ID value.
Problem:
How do I get each generated and displayed button to retain, and then pass along when clicked, its unique "id"?
All of my efforts so far have gotten me results of "undefined" or displaying only the last generated id value, regardless of what button is clicked. Also things that target DOM elements don't seem to help as each of my unique items will not be inside it's own element. Rather just listed out in a single element.
As far as ideas/answers I am after straightforward/readability vs. speed/efficiency. I am also trying to keep as much of my functionality on the javascript side and rely on HTML for as little as possible beyond "displaying" things.
The following code is working as expected for me sans my question:
var allItems = [
{id:1, name:"Space Gem", power:100},
{id:14, name:"Time Gem", power:200},
{id:22, name:"Reality Gem", power:300}
];
var map = {
tile: [
{id:22},
{id:1}
]
}
onTile();
function onTile() {
for ( var i = 0; i < map.tile.length; i++ ) {
var itemId = map.tile[i].id;
for (var j = 0; j < allItems.length; j++) {
if (itemId === allItems[j].id) {
var itemName = allItems[j].name;
var button = document.createElement("button");
button.innerHTML = itemId + " " + itemName;
document.getElementById("tile_display").appendChild(button);
button.addEventListener ("click", get, false);
}
}
}
}
function get(itemId) {
alert ("You clicked button with ID: " + itemId);
}
The only problem I see is that you are passing the same event listener to each newly-created button. And what is more, you are passing the get function but not specifying an argument - which means that itemId will always be undefined when the function runs in response to a click. (I realise now this isn't true - itemId instead will refer to the Event object corresponding to the click event that's just happened - but this is no use to you in this case.)
So all you need to do, I think, is change:
button.addEventListener ("click", get, false);
to:
button.addEventListener ("click", function() {get(itemId);}, false);
EDIT: so this solves the "undefined" problem. But as you noticed, you are getting "id: 1" for both buttons. This is due to the fact that the event listener is a "closure" over its enclosing scope, which here is the onTile function. This means that, when you click the button and the event listener runs, it looks up the value of itemId, which it still has access to even though that scope would otherwise have been destroyed. But there is only one itemId in that scope, and it has whichever value it had when the function finished executing (here 1) - the same value for each event listener.
The simplest fix by far, assuming you are running in ES6-supporting browsers (which these days is all of them, although it always amazes me how many are still using IE which doesn't support it), is simply to change var ItemId = ... to let ItemId = .... Doing this gives ItemId a new scope, that of the loop itself - so you get a different value "captured" each time through the loop - exactly as you want.
In case you do need to support pre-ES6 browsers, you can perform the same "trick" without let, by enclosing the whole body of the outer for loop in a function (this creates a new scope each time), and then immediately invoking it, like this:
function onTile() {
for ( var i = 0; i < map.tile.length; i++ ) {
(function() {
var itemId = map.tile[i].id;
for (var j = 0; j < allItems.length; j++) {
if (itemId === allItems[j].id) {
var itemName = allItems[j].name;
var button = document.createElement("button");
button.innerHTML = itemId + " " + itemName;
document.getElementById("tile_display").appendChild(button);
button.addEventListener ("click", function()
{get(itemId);},
false);
}
}
})();
}
}
function get(itemId) {
alert ("You clicked button with ID: " + itemId);
}
Javascript closures, and in particular how they interact with loops like this, are a tricky topic which has caught many out - so there are loads of SO posts about it. JavaScript closure inside loops – simple practical example is an example, with the answer by woojoo66 being a particularly good explanation.
All that ever needs to happen here is to use the onClick = function() {} property for the newly created button and directly specify the itemId there like so:
button.onclick = function() {
get(itemId);
}
You can easily implement this in a little function like make_button(itemId) { } (see below)
make_button(1);
make_button(2);
make_button(3);
make_button(4);
make_button(5);
function make_button(itemId) {
var button = document.createElement("button");
button.onclick = function() {
get(itemId);
}
button.innerHTML = "button " + itemId;
document.getElementById("mydiv").appendChild(button);
}
function get(_itemId) {
alert("You picked button " + _itemId);
}
<div id="mydiv">
</div>
A much easier way to do this would be to do something like this:
var allItems = [{
id: 1,
name: "Space Gem",
power: 100
},
{
id: 14,
name: "Time Gem",
power: 200
},
{
id: 22,
name: "Reality Gem",
power: 300
}
];
var map = {
tile: [{
id: 22
},
{
id: 1
}
]
}
onTile();
function onTile() {
for (var i = 0; i < map.tile.length; i++) {
var itemId = map.tile[i].id;
/* filter out only items in allItems[] that have id === itemId */
var items = allItems.filter(item => item.id === itemId);
/* loop through those few items and run function make_button */
items.forEach(function(item) {
make_button(item.id, item.name);
});
}
}
/* avoid the use of function names such as 'get' 'set' and other commonly used names as they may conflict with other scripts or native javascript */
function make_button(itemId, itemName) {
var button = document.createElement("button");
button.innerHTML = itemId + " " + itemName;
button.onclick = function() {
get_clicked_tile(itemId); // changed name from 'get'
};
document.getElementById("tile_display").appendChild(button);
}
function get_clicked_tile(itemId) {
alert("You clicked button with ID: " + itemId);
}
<div id="tile_display"></div>

Want to send perticular value to javascript handler whenever clicked

I am creating multiple objects and binding to the same handler. My requirement is I want to send particular value to handle when it is called.
Code:
sliderObj = [];
for(i=0;i<3;i++)
{
sliderObj[i] = this._turntableSlider = new SliderView();
sliderObj[i].on("change:value", this._handleChangeSpeedSlider);
}
When handler is called for sliderObj[0] want to send value 0 similarly 1 for sliderObj[1] etc.
Please help me out how to do that.
Try using .bind():
sliderObj[i].on("change:value", this._handleChangeSpeedSlider.bind(this, i));
Simple demo
Prefer listenTo over on.
Passing the index
Simplest solution would be to wrap the callback into an anonymous function.
But since we're looping and value of i will change, we need to create a simple function which returns a new function using the frozen value of i. (This is similar to using TJ's bind answer)
function getChangeSpeedHandler(index) {
return function() {
this._handleChangeSpeedSlider(index);
}
}
for (i = 0; i < 3; i++) {
var slider = sliderObj[i] = new SliderView();
this.listenTo(slider, "change:value", getChangeSpeedHandler(i));
}
Passing the slider view
Assuming the value of i in the handler is only used to get the right sliderObj, you could pass the slider view directly.
function getChangeSpeedHandler(slider) {
return function() {
this._handleChangeSpeedSlider(slider);
}
}
for (i = 0; i < 3; i++) {
var slider = sliderObj[i] = new SliderView();
this.listenTo(slider, "change:value", getChangeSpeedHandler(slider));
}
Delegating the event handling to the slider view
If you want to handle changes inside the SliderView, it should handle the event itself.
this.listenTo(this.model, 'change:value', this.onValueChange);
Triggering custom events
If it's really the parent that needs to know the slider and their number is dynamic, you could pass the i value to the SliderView and then it could trigger a custom event.
In the slider view:
var SliderView = Backbone.View.extend({
initialize: function(options) {
options = options || {};
this.index = options.index;
// ...
},
onValueChange: function(model, value, options) {
this.trigger('slider:change', value, this.index, this);
},
});
Then in the parent:
initialize: function() {
for (i = 0; i < 3; i++) {
var slider = sliderObj[i] = new SliderView({ index: i });
this.listenTo(slider, "slider:change", this._handleChangeSpeedSlider);
}
},
_handleChangeSpeedSlider: function(value, index, sliderView) {
// handle the change
}

Adding and Removing Event Listeners with parameters

I am writing a vanilla JavaScript tool, that when enabled adds event listeners to each of the elements passed into it.
I would like to do something like this:
var do_something = function (obj) {
// do something
};
for (var i = 0; i < arr.length; i++) {
arr[i].el.addEventListener('click', do_something(arr[i]));
}
Unfortunately this doesn't work, because as far as I know, when adding an event listener, parameters can only be passed into anonymous functions:
for (var i = 0; i < arr.length; i++) {
arr[i].el.addEventListener('click', function (arr[i]) {
// do something
});
}
The problem is that I need to be able to remove the event listener when the tool is disabled, but I don't think it is possible to remove event listeners with anonymous functions.
for (var i = 0; i < arr.length; i++) {
arr[i].el.removeEventListener('click', do_something);
}
I know I could easily use jQuery to solve my problem, but I am trying to minimise dependencies. jQuery must get round this somehow, but the code is a bit of a jungle!
This is invalid:
arr[i].el.addEventListener('click', do_something(arr[i]));
The listener must be a function reference. When you invoke a function as an argument to addEventListener, the function's return value will be considered the event handler. You cannot specify arguments at the time of listener assignment. A handler function will always be called with the event being passed as the first argument. To pass other arguments, you can wrap the handler into an anonymous event listener function like so:
elem.addEventListener('click', function(event) {
do_something( ... )
}
To be able to remove via removeEventListener you just name the handler function:
function myListener(event) {
do_something( ... );
}
elem.addEventListener('click', myListener);
// ...
elem.removeEventListener('click', myListener);
To have access to other variables in the handler function, you can use closures. E.g.:
function someFunc() {
var a = 1,
b = 2;
function myListener(event) {
do_something(a, b);
}
elem.addEventListener('click', myListener);
}
// Define a wrapping function
function wrappingFunction(e) {
// Call the real function, using parameters
functionWithParameters(e.target, ' Nice!')
}
// Add the listener for a wrapping function, with no parameters
element.addEventListener('click', wrappingFunction);
// Save a reference to the listener as an attribute for later use
element.cleanUpMyListener = ()=>{element.removeEventListener('click', wrappingFunction);}
// ...
element.cleanUpMyListener ()
Step 1) Name your function.
Step 2) Save a reference to your function (in this case, save the reference as an attribute on the element itself)
Step 3) Use the function reference to remove the listener
// Because this function requires parameters, we need this solution
function addText(element, text) {
element.innerHTML += text
}
// Add the listener
function addListener() {
let element = document.querySelector('div')
if (element.removeHoverEventListener){
// If there is already a listener, remove it so we don't have 2
element.removeHoverEventListener()
}
// Name the wrapping function
function hoverDiv(e) {
// Call the real function, using parameters
addText(e.target, ' Nice!')
}
// When the event is fired, call the wrapping function
element.addEventListener('click', hoverDiv);
// Save a reference to the wrapping function as an attribute for later use
element.removeHoverEventListener = ()=>{element.removeEventListener('click', hoverDiv);}
}
// Remove the listener
function removeListener() {
let element = document.querySelector('div')
if (element.removeHoverEventListener){
// Use the reference saved before to remove the wrapping function
element.removeHoverEventListener()
}
}
<button onclick="addListener()">Turn Listener on</button>
<button onclick="removeListener()">Turn Listener off</button>
<div>Click me to test the event listener.</div>
To pass arguments to event handlers bind can be used or handler returning a function can be used
// using bind
var do_something = function (obj) {
// do something
}
for (var i = 0; i < arr.length; i++) {
arr[i].el.addEventListener('click', do_something.bind(this, arr[i]))
}
// using returning function
var do_something = obj => e {
// do something
}
for (var i = 0; i < arr.length; i++) {
arr[i].el.addEventListener('click', do_something(arr[i]))
}
But in both the cases to remove the event handlers it is not possible as bind will give a new referenced function and returning function also does return a new function every time for loop is executed.
To handle this problem we need to store the references of the functions in an Array and remove from that.
// using bind
var do_something = function (obj) {
// do something
}
var handlers = []
for (var i = 0; i < arr.length; i++) {
const wrappedFunc = do_something.bind(this, arr[i])
handlers.push(wrappedFunc)
arr[i].el.addEventListener('click', wrappedFunc);
}
//removing handlers
function removeHandlers() {
for (var i = 0; i < arr.length; i++) {
arr[i].el.removeEventListener('click', handlers[i]);
}
handlers = []
}
This can be done quite easily, just not as you have it right now.
Instead of trying to add and remove random anonymouse functions, you need to add or remove a function that handles the execution of your other functions.
var
// Here we are going to save references to our events to execute
cache = {},
// Create a unique string to mark our elements with
expando = String( Math.random() ).split( '.' )[ 1 ],
// Global unique ID; we use this to keep track of what events to fire on what elements
guid = 1,
// The function to add or remove. We use this to handler all of other
handler = function ( event ) {
// Grab the list of functions to fire
var handlers = ( cache[ this[ expando ] ] && cache[ this[ expando ] ][ event.type ] ) || false;
// Make sure the list of functions we have is valid
if ( !handlers || !handlers.length ) {
return;
}
// Iterate over our individual handlers and call them as we go. Make sure we remeber to pass in the event Object
handlers.forEach( function ( handler ) {
handler.call( this, event );
});
},
// If we want to add an event to an element, we use this function
add = function ( element, type, fn ) {
// We test if an element already has a guid assigned
if ( !element[ expando ] ) {
element[ expando ] = guid++;
}
// Grab the guid number
var id = element[ expando ];
// Make sure the element exists in our global cache
cache[ id ] = cache[ id ] || {};
// Grab the Array that we are going to store our handles in
var handlers = cache[id ][ type ] = cache[ id ][ type ] || [];
// Make sure the handle that was passed in is actually a function
if ( typeof fn === 'function' ) {
handlers.push( fn );
}
// Bind our master handler function to the element
element.addEventListener( type, handler, false );
};
// Add a click event to the body element
add( document.body, 'click', function ( event ) {
console.log( 1 );
});
This is just a cut down version of what I've written before, but you can get the gist of it I hope.
Maybe its not perfect solution, but near to ideal, in addition I dont see other ways
Thanks to Kostas Bariotis
Solution key here is:
So what do we do when we need to remove our attached event handlers at some point at runtime? Meet handleEvent, the default function that JavaScript looks for when tries to find a handler that has been attached to an event.
In cas link is broken (I placed first way)
let Button = function () {
this.el = document.createElement('button');
this.addEvents();
}
Button.prototype.addEvents = function () {
this.el.addEventListener('click', this);
}
Button.prototype.removeEvents = function () {
this.el.removeEventListener('click', this);
}
Button.prototype.handleEvent = function (e) {
switch(e.type) {
case 'click': {
this.clickHandler(e);
}
}
}
Button.prototype.clickHandler = function () {
/* do something with this */
}
P.S:
Same tehnics in JS class implementation.
If you develop in typescript you have to implement handleEvent method from EventListenerObject interface
To 'addEventListener' with some parameters, you can use the following code:
{
myButton.addEventListener("click",myFunction.bind(null,event,myParameter1,myParameter2));
}
And the function 'myFunction' should be something like this:
{
function myFunction(event, para1, para2){...}
}

Classes in JavaScript using prototype

I have a problem, I want to create a JavaScript class:
function Calculatore(txt,elements) {
this.p= new Processor();
this.output=txt;
$(elements).click(this.clickHandler);
}
Calculatore.prototype.clickHandler = function() {
var element=$(this);
// Code Here
// "this" contains the element.
// But what if I want to get the "output" var?
// I tried with Calculatore.prototype.output but no luck.
}
So how can I solve this?
You can use jQuery's $.proxy:
function Calculatore(txt,elements) {
this.p= new Processor();
this.output=txt;
$(elements).click($.proxy(this.clickHandler, this));
}
Calculatore.prototype.clickHandler = function(event) {
var clickedElement = event.target;
alert(this.output);
}
Edited. Jason brought up a good point in the comments. It's probably better to use event.target which references only the element clicked, rather than elements which may reference an array of objects matching the selection.
You have a collision between this values. You currently don't have access to the instance because this has been set to the element inside a click handler.
You could make a proxy function to pass both the this value (the element) and the instance:
function Calculatore(txt,elements) {
this.p= new Processor();
this.output=txt;
var inst = this; // copy instance, available as 'this' here
$(elements).click(function(e) {
return inst.clickHandler.call(this, e, inst); // call clickHandler with
// 'this' value and 'e'
// passed, and send 'inst'
// (the instance) as well.
// Also return the return
// value
});
}
Calculatore.prototype.clickHandler = function(e, inst) {
var element = $(this);
var output = inst.output;
};

Categories