Can only click jQuery element once before it stops working - javascript

I've got the following code:
$("#another").click(function() {
$('#another').replaceWith('<a id="another" class="btn btn-primary btn-mini disabled"><i class="icon-refresh icon-white"></i> Loading...</a>');
$.get('another.php', { 'cycle' : i }, function(data) {
$('tbody').append(data);
$("#another").replaceWith('<a id="another" class="btn btn-primary btn-mini"><i class="icon-plus icon-white"></i> Load another cycle</a>');
});
i++;
});
When I click the element with the id of another, it loads once. After one click, it won't work again.

You're replacing the node with a node that doesn't have the event listener.
Basically before the click you have
[#another]
^
|
[clickListener]
You then build another button (<a id="another" class="btn btn-primary btn-mini disabled"><i class="icon-refresh icon-white"></i> Loading...</a>)
[#another] [#another](2)
^
|
[clickListener]
then we replace the first another with a second one in the layout:
[#another] [#another](2)
^
|
[clickListener]
oh wait, nothing changed in my model. That's because the click listener was linked to that first object ( that is no longer visible), whereas the visible one is still there.
So codewise, what does this mean? It simply means you'll need to attach the event listener back on there. Here's how I'd have done it
var onClick=function(){
$('#another').replaceWith('<a id="another" class="btn btn-primary btn-mini disabled"><i class="icon-refresh icon-white"></i> Loading...</a>')
.click(onClick); // <--- this is the important line!
$.get('another.php', { 'cycle' : i }, function(data) {
$('tbody').append(data);
$("#another").replaceWith('<a id="another" class="btn btn-primary btn-mini"><i class="icon-plus icon-white"></i> Load another cycle</a>');
});
i++;
}
$("#another").click(onClick);

If you replace the element with another, all listeners will be removed. To avoid this you either add the listener again to the new element
$('#another').bind('click', function() {
//do something
});
or move the code to a function and add a onclick attribute to your element.
onclick="my_function();"
in your current javascript it would be
$('#another').replaceWith('<a id="another" class="btn btn-primary btn-mini disabled" onclick="my_function();"><i class="icon-refresh icon-white"></i> Loading...</a>');

It's best to just keep the same button, with the same event handler. Just dynamically change the text and increment i. Try this:
// Execute in a closure to isolate all the local variables
// Optional, but I like doing this when counter variables are involved
(function() {
var button = $("#another");
var a = button.find("a");
var i = 1;
button.click(function() {
// Replace the inner html, not the entire element
a.html("<i class='icon-refresh icon-white'</i> Loading...");
$.get("another.php", {
cycle: i
}, function(data) {
$("tbody").append(data);
a.html("<i class='icon-plus icon-white'></i> Load another cycle");
i++;
});
});
})();
The benefit of this method is that there is less DOM manipulation, no inline JavaScript, and no global functions or variables. There really is no reason to destroy the button each time and recreate it if the outer markup is the same.

Related

How do I change what a JS function does when called by different buttons?

I am learning Javascript and I'm trying to clean up my code. The code is pretty simple: it simply changes the color of some text by clicking some different buttons. When you click the red button the text turns red, the blue button the text turns blue, etc. Here is the code:
HTML:
<h1 id="title">Change my color!</h1>
<button id="btn" onclick="colorRed()">Red</button>
<button id="btn" onclick="colorGreen()">Green</button>
<button id="btn" onclick="colorBlue()">Blue</button>
<button id="btn" onclick="colorBlack()">Black</button>
Javascript:
var title = document.getElementById("title");
function colorRed() {
title.style.color = "red";
}
function colorGreen() {
title.style.color = "green";
}
function colorBlue() {
title.style.color = "blue";
}
function colorBlack() {
title.style.color = "black";
}
This code works. My question is how do I clean up my Javascript; in a case where I would've had 20 buttons, coding 20 different functions would obviously not be the way to go.
I did try the following for every single color, but that didn't work:
Javascript:
var title = document.getElementById("title");
var btn = document.getElementById("btn");
function changeColor() {
if(btn.innerHTML == "Red") {
title.style.color = "red";
} else if ...
}
I think it goes wrong when I try to identify which button has been clicked by seeing if their inner HTML is equal to a certain color, but I'm not sure how to fix that. Help would be much appreciated!
EDIT: My question isn't a duplicate of Change an element's background color when clicking a different element as the code I wrote works already, and I just want to learn how to clean it up.
Might be easiest to just make one changeColor function and pass it a color in the event:
var title = document.getElementById("title");
function changeColor(color) {
console.log(color);
title.style.color = color;
}
<h1 id="title">Change my color!</h1>
<button class="btn" onclick="changeColor('red')">Red</button>
<button class="btn" onclick="changeColor('green')">Green</button>
<button class="btn" onclick="changeColor('blue')">Blue</button>
<button class="btn" onclick="changeColor('black')">Black</button>
Side note: you really shouldn't repeat id's and should use class instead.
It is generally not advisable to use inline event handlers, use addEventListener instead. Rather than adding an event listener for each element, I would recommend adding a common parent element, attaching one event listener to that and inspect the event ("event delegation") to determine which color to apply:
var title = document.querySelector('#title');
document.querySelector('#button-container').addEventListener('click', function(event) {
var color = event.target.getAttribute('data-color')
title.style.color = color;
}, false)
<h1 id="title">Change my color!</h1>
<div id="button-container">
<button data-color="red">Red</button>
<button data-color="green">Green</button>
<button data-color="blue">Blue</button>
<button data-color="black">Black</button>
</div>
Don't use inline HTML event attributes, such as onclick. There are a variety of reasons why and if you are just starting with JavaScript, you don't want to pick up any bad habits. Instead, keep your JavaScript completely separate from your HTML and follow modern standards using the .addEventListener() JavaScript method for setting up event handlers.
Also, id values must be unique within a document (the whole point of them is to uniquely identify elements). To be able to group just the buttons that relate to this operation, you can give them all the same CSS class and then query on that class in JavaScript (shown below).
Next, you only need one function, but if your individual buttons were storing the color they should produce, that one function could extract it and use it without the need for any arguments to be passed to your function:
var title = document.getElementById("title");
// When you query your document for groups of matching elements (using methods
// like: getElementsByTagName or getElementsByClassName) you get back an object
// that is similar to an array, called a "node list". Although these "array-like"
// objects support some of the standard array object's features, they are not
// true arrays and don't implement many of the powerful array methods out there.
// But, we can convert the node list returned from .querySelectorAll into an array
// and then we can iterate the array with .forEach() looping method later.
var buttonArray = Array.prototype.slice.call(document.querySelectorAll(".colorBtn"));
// Loop through the button array (the function provided as an argument will be
// executed for each element in the array)
buttonArray.forEach(function(button){
// Set up click event handlers for each button
button.addEventListener("click", function(){
// Just set the color to the "data-color" attribute value on the element
title.style.color = button.dataset.color;
})
});
.colorBtn {
box-shadow:2px 2px 1px grey;
border-radius:4px;
width:100px;
display:inline-block;
margin:4px;
}
.colorBtn:hover, .colorBtn:active {
box-shadow:-2px -2px 1px #e0e0e0;
outline:none;
}
<h1 id="title">Change my color!</h1>
<!-- Putting related elements into the same class allows you to
not only style them identically, but also find them in
JavaScript more easily. -->
<button id="btn1" class="colorBtn" data-color="red">Red</button>
<button id="btn2" class="colorBtn" data-color="green">Green</button>
<button id="btn3" class="colorBtn" data-color="blue">Blue</button>
<button id="btn4" class="colorBtn" data-color="black">Black</button>
Another way to do this is the following:
function changeColor() {
title.style.color = btn.style.backgroundColor;
}
and set each buttons background to the appropriate color.
Rather than have the function figure out what color to change to, you could pass that color to the changeColor function. Your function would become:
function changeColor(color) {
title.style.color = color;
}
Then in your HTML, you would change the onclick properties to pass that in:
<button id="btn" onclick="changeColor('red')">Red</button>
...
Also, I wanted to mention that your issues before weren't just related to checking the innerHTML like you suggested in your post. One issue would be that you have multiple HTML elements with the same id. That isn't going to work well when using document.getElementById().
When you need a dynamic number of elements, we need an easy way to find them in the DOM. There are some helper methods out there, like getElementsByClassName() on the document object. The first thing I would do is drop the IDs on your buttons (which should be unique, by the way), in favor of a class name:
<button class="btn" onclick="colorRed()">Red</button>
<button class="btn" onclick="colorGreen()">Green</button>
<button class="btn" onclick="colorBlue()">Blue</button>
<button class="btn" onclick="colorBlack()">Black</button>
The second thing I would do is refactor this so that you can use good unobtrusive JavaScript practices (basically getting the JavaScript good out of the HTML markup and wiring it up all in a JavaScript code block). First, we need a way for the JavaScript to know which color you want to change the button text to. Let's introduce a custom HTML data- attribute in place of the hard-coded onclick handlers:
<button class="btn" data-color="red">Red</button>
<button class="btn" data-color="green">Green</button>
<button class="btn" data-color="blue">Blue</button>
<button class="btn" data-color="black">Black</button>
Now, we need a way to find these buttons so we can loop over them and apply an onclick handler. This can be done with the method I mentioned earlier:
var title = document.getElementById("title");
var buttons = document.getElementsByClassName("btn");
This will build you an array of the four buttons (or however many you have).
Then we need to loop over them and assign a click handler that retrieves the color from the data-color attribute and assigns it to the <h1> element's style.color property:
for (var btnIndex = 0; btnIndex < buttons.length; btnIndex++)
{
buttons[btnIndex].onclick = function() {
title.style.color = this.getAttribute('data-color');
}
}
And that's it! We eliminated all of the duplicate and practiced some better JavaScript techniques at the same time. Try the code out below:
var title = document.getElementById("title");
var buttons = document.getElementsByClassName("btn");
for (var btnIndex = 0; btnIndex < buttons.length; btnIndex++)
{
buttons[btnIndex].onclick = function() {
title.style.color = this.getAttribute('data-color');
}
}
<h1 id="title">Change my color!</h1>
<button class="btn" data-color="red">Red</button>
<button class="btn" data-color="green">Green</button>
<button class="btn" data-color="blue">Blue</button>
<button class="btn" data-color="black">Black</button>

How to assign a javascript function by classname

I have multiple buttons (generated by php) for a shopping cart application:
<button class="addtocart" id="<?php echo $code; ?>">
<span id="addtocartbutton">Add to cart</span>
</button>
I want to update my cart using a function:
function AddtoCart() {
alert("Added!");
}
Later, I want to find the id ($code) created by the button which called it (not sure how to do that also, but maybe that's another question). And so I tried this:
document.getElementsByClassName("addtocart").addEventListener("click", AddtoCart());
But it doesn't work. It was working using an onclick, but I understand that the right way to do it by creating an EventListener. Also, I cannot use the on() function in jQuery, because I am forced to use jQuery Version 1.6 which does not have it.
I have looked at https://stackoverflow.com/a/25387857/989468 and I can't really assign it to the parent which is a p tag, because I obviously don't want the other elements in the p tag to be assigned this function.
While the answers given are correct, there is another way: Event Delegation
Attach the listener to a SINGLE thing, in this case the document body and then check to see what element was actually clicked on:
Warning: Typed on the fly: Untested
// Only needed *once* and items may be added or removed on the fly without
// having to add/remove event listeners.
document.body.addEventListener("click", addtoCart);
function addtoCart(event) {
var target = event.target;
while(target) {
if (target.classList.contains('addtocart')) {
break;
}
// Note: May want parentElement here instead.
target = target.parentNode;
}
if (!target) {
return;
}
var id = target.dataset.id;
alert(id + " added!");
}
You should attach click event to every element with class addtocart, since getElementsByClassName() return an array of all objects with given class name so you could use for to loop through everyone of them and associate it with function you want to trigger on click (in my example this function called my_function), check example bellow :
var class_names= document.getElementsByClassName("addtocart");
for (var i = 0; i < class_names.length; i++) {
class_names[i].addEventListener('click', my_function, false);
}
Hope this helps.
function my_function() {
alert(this.id);
};
var class_names= document.getElementsByClassName("addtocart");
for (var i = 0; i < class_names.length; i++) {
class_names[i].addEventListener('click', my_function, false);
}
<button class="addtocart" id="id_1">button 1</button>
<button class="addtocart" id="id_2">button 2</button>
<button class="addtocart" id="id_3">button 3</button>
<button class="addtocart" id="id_3">button 4</button>
I'll show some of the errors you had in your code, then I'll show you how can you improve it so that you can achieve what you want, and I also show that it works with buttons dynamically added later:
First and foremost, you need to pass the function reference (it's name) to the addEventListener! You have called the function, and passed whatever it returned. Instead of:
document.getElementsByClassName("addtocart").addEventListener("click", AddtoCart());
It should've been:
document.getElementsByClassName("addtocart").addEventListener("click", AddtoCart);
Second: document.getElementsByClassName("addtocart") returns a NodeList, you can't operate on it, you need to operate on it's elements: document.getElementsByClassName("addtocart")[0], [1],....
Third, I would suggest you to use the data-... html attribute:
<button class="addtocart" id="addtocart" data-foo="<? echo $code; ?>">
This way you can pass even more data. Now you can get the $code as:
document.getElementById('addtocart').dataset.foo
// el: the button element
function AddtoCart(el) {
// this is the id:
var id = el.id;
// and this is an example data attribute. You can have as many as you wish:
var foo = el.dataset.foo;
alert(id + " (" + foo + ") added!");
}
// Try add a div or something around the area where all the buttons
// will be placed. Even those that will be added dynamically.
// This optimizes it a lib, as every click inside that div will trigger
// onButtonClick()
document.getElementById("buttons").addEventListener("click", onButtonClick);
// this shows that even works when you dynamically add a button later
document.getElementById('add').onclick = addButton;
function addButton() {
var s = document.createElement("span");
s.text = "Add to cart";
var b = document.createElement("button");
b.innerHTML = 'Third <span class="addtocartbutton">Add to cart</span>';
b.className = "addtocart";
b.id="third";
b.dataset.foo="trio";
// note the new button has the same html structure, class
// and it's added under #buttons div!
document.getElementById("buttons").appendChild(b);
}
// this will gett triggered on every click on #buttons
function onButtonClick(event) {
var el = event.target;
if (el && el.parentNode && el.parentNode.classList.contains('addtocart')) {
// call your original handler and pass the button that has the
// id and the other datasets
AddtoCart(el.parentNode);
}
}
<div id="buttons">
<button class="addtocart" id="first" data-foo="uno">First <span class="addtocartbutton">Add to cart</span></button>
<button class="addtocart" id="second" data-foo="duo">Second <span class="addtocartbutton">Add to cart</span></button>
</div>
<button id="add">Add new button</button>
<html>
<head>
<script>
window.onload=function{
var btn = document.getElementsByName("addtocartbtn")[0];
btn.addEventListener("click", AddtoCart());
}
function AddtoCart() {
alert("Added!");
}
</script>
</head>
<body >
<button class="addtocart" name ="addtocartbtn" id="<?php echo $code; ?>" > <span id="addtocartbutton">Add to cart</span></button>
</body>
</html>
Actually class in Javascript is for multiple selection you should provide index like an array.
<button class="addtocart"> <span id="addtocartbutton">Add to cart</span></button>
<script type="text/javascript">
document.getElementsByClassName("addtocart")[0].addEventListener("click", AddtoCart);
function AddtoCart() {
alert("Added!");
}
</script>
Also your second parameter was wrong don't use parentheses.
Applying parentheses means it will call the function automatically when loaded, and will not call the function after that.

AngularJS - how to add a conditional to bootstrap modal target?

So I have a modal that appears when a button is clicked - however I want the modal ONLY to appear when certain conditions are met (which are defined in my controller).
HTML CODE:
<button class="btn btn-primary-outline" type="button" data-uk-modal="{target:'#previewModal'
}" ng-click="previewOfferBefore()">Preview</button>
The above works (modal with the id 'previewModal' pops out on click). So my approach is to add the conditional in the controller and define the value of the "target" in there using Angular data binding.
ie:
<button class="btn btn-primary-outline" type="button" data-uk-modal="{target: {{ previewLink
}}}" ng-click="previewOfferBefore()">Preview </button>
Then the controller would have:
$scope.previewOfferBefore = function() {
if (/*some conditions here*/) {
$scope.previewLink = '#'; /*don't let a modal pop up */
}
else {
$scope.previewLink = '#previewModal' /*let the modal pop up */
}
}
One approach I tried as well is using ng-href instead of bootstrap's data-uk-modal and that also didn't work. I know my controller function is fine since when I place {{ previewLink }} inside a p html tag it prints out the right id I want. So the issue is how I'm binding the data inside the button class.
If you're okay with the button being disabled or greyed out, then one solution would be to use ng-disabled. Your controller would look something like this;
$scope.previewOfferBefore = function() {
if (/*some conditions here*/) {
$scope.canClick= true;
}
else {
$scope.canClick= false;
}
}
and your html would then become;
<button ng-disabled="canClick" class="btn btn-primary-outline" type="button" data-uk-modal="{target:'#previewModal'
}" ng-click="previewOfferBefore()">Preview</button>
Then your button will become unclickable if it hits the false case on the if statement.
You could simply have two buttons and use ng-if or ng-show to just show one of them depending on your expression. You can simply use angular inside.
<button ng-show="previewLink !== '#'" type="button" data-uk-modal="{target: '#previewLink'}" ng-click="previewOfferBefore()">
Preview
</button>
<button ng-show=""previewLink === '#'"" type="button" data-uk-modal="{target: '#'" ng-click="previewOfferBefore()">
Preview
</button>
Alternative:
I would try ng-attr to get your angular expression into the attribute like
ng-attr-uk-modal="{{'target:' + value}}"
and bind value to the target you need. I haven't tried this, might need some adjustments.

Toggle icon on button via JavaScript

I have a Bootstrap webpage set up which displays a collapsible table. Each table row has a button which has a glyphicon-chevron-down icon. When a user clicks on this button, that icon needs to change to glyphicon chevron-up. I have tried doing this a few different ways to no avail.
Current set up is:
<script >
function toggleChevron(button) {
if (button.find('span').hasClass('glyphicon-chevron-down')) {
button.find('span').className = "glyphicon glyphicon-chevron-up";
}
if (button.find('span').hasClass('glyphicon-chevron-up')) {
button.find('span').className = "glyphicon glyphicon-chevron-down";
}
}
</script>
<button type="button" onclick="toggleChevron(this)" class="btn btn-default">
<span class="glyphicon glyphicon-chevron-down"></span>
</button>
JSFiddle
This works:
toggleChevron = function(button) {
$(button).find('span').toggleClass('glyphicon-chevron-down glyphicon-chevron-up');
}
The main issue is that you are passing this, which isn't a jQuery object, so you need to wrap button in the jQuery function $(button)
http://jsfiddle.net/V9LSS/4/
There was some errors with your jsfiddle. Check the console.
Here's how I did it though:
http://jsfiddle.net/V9LSS/5/
var span = $('.glyphicon');
$('.btn').click(function(e){
if(span.hasClass('glyphicon-chevron-down'))
span.removeClass('glyphicon-chevron-down').addClass('glyphicon-chevron-up');
else
span.removeClass('glyphicon-chevron-up').addClass('glyphicon-chevron-down');
});
Simplified a little:
http://jsfiddle.net/V9LSS/7/
var span = $('.glyphicon');
$('.btn').click(function(e){
span.toggleClass('glyphicon-chevron-down glyphicon-chevron-up');
});
function toggleChevron(button) {
if ($(button).find('span').hasClass('glyphicon-chevron-down')) {
$(button).find('span').removeClass("glyphicon-chevron-down").addClass("glyphicon-chevron-up");
}else{
$(button).find('span').removeClass("glyphicon-chevron-up").addClass("glyphicon-chevron-down");
}
}
You are passing the dom node as a parameter and trying to use jquery functions on it,you need to pass a jquery element in the onclick($(this) instead of just this).
Try this:
$('button').click(function(e) {
var $elm = $(this).find('span'),
cDown = 'glyphicon-chevron-down',
cUp = 'glyphicon-chevron-up';
if ($elm.hasClass(cDown)) {
$elm.removeClass(cDown).addClass(cUp);
}
else {
$elm.removeClass(cUp).addClass(cDown);
}
});
http://jsfiddle.net/V9LSS/3/
Or if you're feeling gutsy enough to inline everything (idea courtesy of dave):
<button type="button" onclick="$(this).find('span').toggleClass('glyphicon-chevron-down glyphicon-chevron-up')" class="btn btn-default">
<span class="glyphicon glyphicon-chevron-down"></span>
</button>
http://jsfiddle.net/V9LSS/9/

jQuery data $(...).data('foo') sometimes returning undefined in Backbone app

Running into a weird bug – so I have an attribute defined as 'data-tagtype' on my button elems in my HTML. When a user clicks on a button, the following method gets called:
onClickTag: function(e) {
if (!this.playerLoaded) {
return false;
}
var type = $(e.target).data('tagtype');
var seconds = this.getCurrentTime();
console.log(type);
if (type) {
this.model.addTag({
type: type,
seconds: seconds
});
}
},
This works most of the time, but for some reason sometimes type is undefined for (seemingly) random elements. The corresponding HTML is here:
<button id="tag-love" class="tagger disabled" data-tagtype="love"><i class="fa fa-heart fa-fw"></i></button>
<button id="tag-important" class="tagger disabled" data-tagtype="important"><i class="fa fa-exclamation fa-fw"></i> Important</button>
<button id="tag-more" class="tagger disabled" data-tagtype="more"><i class="fa fa-ellipsis-h fa-fw"></i> More</button>
<button id="tag-confused" class="tagger disabled" data-tagtype="confused"><i class="fa fa-question fa-fw"></i> Confused</button>
It's weird because there doesn't seem to be a pattern with respect to which ones return undefined when. Sometimes all of them work and sometimes one of them returns undefined for a couple of seconds, but then if I keep clicking it returns the proper value.
The View is definitely rendered/loaded into the DOM before any of these methods get called.
Any ideas? Does Backbone do something maybe?
The problem is that Backbone views use event delegation for their event handling. That means that e.target will be the the element that is clicked rather than the element that is responding to the event. If you click on the <i>, e.target will be that <i> but if you click on the text, e.target will be the <button>; the <i> doesn't have the data attribute you're looking for but the <button> does. That means that sometimes $(e.target).data('tagtype') will be undefined.
You can see this behavior in a simple example:
<div id="x">
<button type="button" data-tagtype="love"><i>icon</i> text</button>
</div>
and a minimal view:
var V = Backbone.View.extend({
el: '#x',
events: {
'click button': 'clicked'
},
clicked: function(ev) {
console.log(
$(ev.target).data('tagtype'),
$(ev.currentTarget).data('tagtype')
);
}
});
Demo: http://jsfiddle.net/ambiguous/pe77p/
If you click on <i>icon</i>, you'll get undefined love in the console but if you click on the text, you'll get love love in the console.
That little demo also contains the solution: use e.currentTarget instead of e.target.

Categories

Resources