I have a form button in my web app, (purely made with Vanilla Js), The form is used to get data from user and when button is clicked, that data goes to a HTML table and so on.
To edit Table Row, I have placed Edit Button on the table column (at every row), image attached below would help to get clear idea:
The Blue Submit Button is Named as "addButton" , when the user clicks "Edit" of particular row in table, corresponding data is Shown In Input Field, So user can fill in new details.
Up to here is everything fine, now the real problem begins: After user edits the info in Input fields, Blue Submit button is used for saving those changes and spontaneously show change in table as well. (this is the required behaviour).
Instead what is happening is : The modified row is shown in table as well as a new entry is made in row with same details ("addButton" event listener is executed twice, once for saving changes done by user, and once for adding new item in row).
I have placed this event listener (same name, because I do not want separate buttons for saving edited info) twice.
For clarity, please look at code:
(this is first addButton event listener in global scope - used for adding new entry to table)
addButton.addEventListener("click", function(e){
var obj = {
id : object.count,
date : formatDate(inDate.value, inMonth.value),
day : inDay.value,
item : inItem.value,
price : parseInt(inPrice.value),
};
object.info.push(obj);
object.count += 1;
refreshDOM();
});
For "Edit and Delete" buttons in row, I have this:
modifyBtn is tied to table, so I can get click target with that.
(Inside modifyBtn second addButton event listener is present - used for saving changes and show output in table)
modifyBtn.addEventListener('click', function(e){
if(e.target.classList[0] === 'editInfo'){
var eid = e.target.getAttribute('data-id');
var eindex = getIndex(eid);
inDay.value = object.info[eindex]['day'];
inDate.value = parseInt(object.info[eindex]['date'].substr(0,2));
inMonth.value = object.info[eindex]["date"].substr(4);
inItem.value = object.info[eindex]['item'];
inPrice.value = object.info[eindex]['price'];
addButton.addEventListener("click", function(e){
object.info[eindex]['day'] = inDay.value;
object.info[eindex]['date'] = formatDate(inDate.value, inMonth.value);
object.info[eindex]['item'] = inItem.value;
object.info[eindex]['price'] = parseInt(inPrice.value);
refreshDOM();
});
}
if(e.target.classList[0] === 'deleteInfo'){
var did = e.target.getAttribute('data-id');
var dindex = getIndex(did);
console.log(dindex);
object.info.splice(dindex,1);
refreshDOM();
}
});
So after editing when user clicks blue submit button, I want only that addButton event listener to execute which is inside modifyBtn event listener,
Currently both addButton event listeners are getting executed. Sorry for Huge explanation.
The issue, as you're likely aware, is that you're assigning multiple click listeners to the same element (a behaviour that you want) at the same time (a behaviour you don't want). There are a couple of ways you could fix this.
The fastest fix
The quickest way to solve your problem would be to use .removeEventListener(). With this, you can remove the previous click listener (the one that adds a new element to the info array) before you create the second click listener that sets the edited data.
At the end of the second click listener function, you would rebind the first click listener again so the behaviour returned to normal.
Pros of this solution
It's the fastest way to solve the problem you're having
Cons of this solution
Unbinding and rebinding event listeners can make it hard to reason about your code
In order to remove an event listener, you need to supply the original function (like .removeEventListener("click", listenerFunction). As you are currently using an anonymous function expression, you'll have to move the functions currently inside click listeners elsewhere and name them (so that you can pass them to the removeEventListener function
It's not actually clear which event listener is bound to addButton at any one time
The solution
We need to move the function declaration for addButton outside of .addEventListener and give it a name. I've called it addItemClickHandler but you can call it whatever you want:
function addItemClickHandler(e) {
var obj = {
id : object.count,
date : formatDate(inDate.value, inMonth.value),
day : inDay.value,
item : inItem.value,
price : parseInt(inPrice.value),
};
object.info.push(obj);
object.count += 1;
refreshDOM();
}
addButton.addEventListener("click", addItemClickHandler);
This should work exactly the same. Now we need to move the second event listener function you're trying to add into its own named function. As we're only referring to the name of the function from inside itself, we don't even need to move it out, just give it a name. I'm going to give it editItemClickHandler:
addButton.removeEventListener("click", addItemClickHandler);
addButton.addEventListener("click", function editItemClickHandler(e){
object.info[eindex]['day'] = inDay.value;
object.info[eindex]['date'] = formatDate(inDate.value, inMonth.value);
object.info[eindex]['item'] = inItem.value;
object.info[eindex]['price'] = parseInt(inPrice.value);
refreshDOM();
addButton.removeEventListener("click", editItemClickHandler);
addButton.addEventListener("click", addItemClickHandler);
});
As you can see, we first remove the listener addItemClickHandler so that when you click on the "Add" button, it won't do anything
We then bind a different click listener, that we give the name editItemClickHandler so we can remove it after the edit is complete
We do all the edits we need to do
Finally, we remove the new edit click listener we created and re-add the original click listener, so the functionality goes back to normal
The more robust fix
Here is a codepen of your application after applying the following fixes.
The above solution is the fastest way to fix your problem, but there are more robust ways to ensure a working solution. In this solution, I'm going to tidy up some of your code in order to make it cleaner and easier to understand.
Pros of this solution
We won't have to unbind or rebind any click listeners
It's easier to reason about what's happening
Cons of this solution
It'll take longer to implement, as it requires restructuring more of your code
The solution
Step 1: Keep track of whether we're editing or not
Firstly, as we're not rebinding click listeners, we need to keep track of what we're editing. Let's create an object called editing just below object:
var editing = {
mode: false,
index: null
};
This will let us keep track of whether or not we're editing anything (editing.mode), and what the index of the item we're editing is (editing.index).
Step 2: Update the addButton event listener to use the editing object
Next, we need to modify our addButton.addEventListener to use this new editing object:
addButton.addEventListener("click", function(e){
if (editing.mode) {
var info = object.info[editing.index];
info['day'] = inDay.value;
info['date'] = formatDate(inDate.value, inMonth.value);
info['item'] = inItem.value;
info['price'] = parseInt(inPrice.value);
editing.mode = false;
} else {
var obj = {
id : object.count,
date : formatDate(inDate.value, inMonth.value),
day : inDay.value,
item : inItem.value,
price : parseInt(inPrice.value),
};
object.info.push(obj);
object.count += 1;
}
refreshDOM();
});
If editing.mode is true, when the addButton is clicked, it will update the values and then disable editing.mode, putting it back to the way it was before
If editing.mode is false, it will simply add the item to the array (same code as you had before)
No matter what happens, the DOM will get refreshed
Step 3: Update the modifyBtn event listener to use the editing object
Also, I've noticed you're using classes to modify programmatic behaviour instead of data- attributes. This is fine in a lot of cases, but for your exact use case of determining the behaviour, it's recommended to use data- attributes instead. We should also set the href's to #, as we don't need to use them as links:
<td>Edit | Delete</td>
Now, I've restructured your modifyBtn.addEventListener() to handle a these aspects differently:
modifyBtn.addEventListener('click', function(e){
e.preventDefault();
var el = e.target;
var id = parseInt(el.dataset.id);
var index = object.info.findIndex(item => item.id === id);
var info = object.info[index];
var action = el.dataset.action;
if (action === "edit") {
editing.mode = true;
editing.index = index;
inDay.value = info['day'];
inDate.value = parseInt(info['date'].substr(0,2));
inMonth.value = info["date"].substr(4);
inItem.value = info['item'];
inPrice.value = info['price'];
}
if (action === "delete") {
object.info.splice(index, 1);
}
refreshDOM();
});
Using e.preventDefault() means that the browser wont navigate to the # href when clicking on the link
I've moved duplicate code where it retrieved eid and eindex no matter what action you were performing ('editing' or 'adding') outside of those functions
As we are now editing something, we set the editing object to have enabled: true and whatever the index of the current item we're editing is
Instead of using object.info[eindex] every time, I've assigned it to a variable
You no longer need to keep your getIndex function, as you can use Array.prototype.findIndex() instead
The recommended way to get data- attributes is to use element.dataset.name instead of element.getAttribute()
As is standard now, no matter what happens the DOM will get refreshed
Step 4: Add a dynamic form header
Alright, awesome! So this totally works. One final thing I'd like to do is make it more clear what is happening, so in your index.html under your <div class="form-modal">, I'm going to add an ID to your h2:
<h2 id="form-header">Add Item</h2>
Then, back at the top of index.js:
formHeader = getElement('form-header');
And then finally in refreshDOM():
formHeader.textContent = editing.mode ? "Edit Item" : "Add Item";
This will update the text inside <h2 id="form-header"></h2> to depending on whether or not it's being edited. This is a ternary operator, and is often used as a quick way to choose different outcomes depending on a boolean variable.
I hope this wasn't too much information. I've spent a while looking at your code and really wanted to help with best practices and such! Let me know if you have any questions!
Related
I am a teacher with the worst possible slow gradebook, so much so that I would like to use some code to automate it. Basically to submit a grade I need to:
Find the cell box and click on it once to show the submit button
Then click on the button
HOWEVER: Every time you click on the box or button it gives it a new html ID. Therefore I need some code that looks for all the boxes and buttons and hits them. I am not sure how to do this without a static ID.
My code is most definitely formatted improperly, I am a total beginner.
var selectorBox = ['name_of_cell']
var selectorCollection = ['name_of_button']
selectorBox.forEach((s) =>{
let element = document.querySelector(s);
if(element) element.click();
else console.warn('No element found for the supplied selector:', s);
});
selectorCollection.forEach((s) =>{
let element = document.querySelector(s);
if(element) element.click();
else console.warn('no element found for the supplied selector:', s);
});
I need help:
Reformatting to the proper syntax / spacing etc.
Writing a function that finds and clicks on the box THEN the button (the above works).
Making my code look for the boxes then buttons, however as mentioned above the ID for each box and button switches every time you click on one, and for each different class i have (I have about 400 students.)
If you can select cells/buttons by class, you can loop through all cells, "click" them, and then "click" the button once it appears.
var cells = document.querySelectorAll('.x-grid-cell');
cells.foreach((cell) => {
cell.click();
var button = document.querySelector('.class_of_your_button')
button.click()
})
I have one more input box - ibox2, on the same page.
Problem - After doing anything on ibox1 and leaving value of length > 5 there, if I start typing in ibox2 the focus jumps back to ibox1.
It is that if loop with ibox1.focus() that is doing it. How could I remove focus entirely from ibox1 upon clicking outside and nullify the if loop and its statements.
I tried blurbut it did not work.
var ibox1 = $("#inputbox1");
$(document).on("change", ibox1,function(e) {
var valu = ibox1.val();
if(valu.length > 5){
#do something
ibox1.focus(); #used this as input box lost focus with each charater typed.
}
});
var ibox2 = $("#inputbox2"); #This is for google places autocomplete.
PS - Please do not tag it as a duplicate one, I have tried almost everything here, and only then I posted this. I shall remove it upon getting solution.
Respected mods, I followed a nice accepted answer and made a mistake about understanding $('document'), but I now got it cleared. That's the reason I am not deleting this question, even though I said I would, as it might help others. You guys, if
you feel, could delete this. Thanks.
The focus is jumping back to ibox1 because you are instructing your document to do so each time the onChange event is fired.
e.g.: $(document).on("change", ibox1, funct... where you are calling for ibox1.focus(); `.
Possible solution: bind your change event to the element of interest itself and avoid binding an event of such local significance to the whole document in the future.
Use a simple method to attach an event to inputs. Check below code it may help you.
(function(){
var in1 = jQuery('#input1'); // first input
var in2 = jQuery('#input2'); // second input
// On change of first input
in1.change(function(){
if(this.val().length > 5){
// do something
}
});
// On change of second input
in2.change(function(){
if(this.val().length > 5){
// do something
}
});
})();
I have some issue understanding the jQuery().change() behavior.
The HTML is basically a lot of input elements - checkboxes ( each with ID of id^='o99-post-visible-XXX' - and they are pure CSS images as Checkboxes, but this is irrelevant ) and I have another checkbox ("#o99-check-all") to "check all" and a text input field ('#o99-post-visible-ids') that receives the IDs of the selected boxes.
The jQuery code is as follows:
jQuery(document).ready(function() {
jQuery("#o99-check-all").change(function () {
jQuery("input:checkbox[id^='o99-post-visible-']").prop('checked', jQuery(this).prop("checked")).trigger('change');
});
var checkboxes = jQuery("input.o99-cbvu-posts-checkbox:checkbox");
checkboxes.on('change', function() {
// get IDS from all selected checkboxes and make a comma separated string
var ids = checkboxes.filter(':checked').map(function() {
return this.value;
}).get().join(',');
// put IDS inside a text input field
jQuery('#o99-post-visible-ids').val(ids);
// console.log(ids);
});
});
Now, more or less everything works now, but that is not the issue.
at first , the first chunk of code was:
jQuery("#o99-check-all").change(function () {
// without .trigger('change') chained
jQuery("input:checkbox[id^='o99-post-visible-']").prop('checked', jQuery(this).prop("checked"));
});
and it did not work ( why?? ) - meaning the boxes were selected as expected but the '#o99-post-visible-ids' input field was not receiving the IDs - until I chained a .trigger('change') event - when suddenly it works well.
my confusion is with the following ( which perhaps for my little understanding of jQuery internal works is counter-intuitive )
after chain adding .trigger('change') - isn't it somehow an endless loop where a chain() event is trigger inside a listener of change() ? and if not why?
Why is the code functioning now and did not function correctly before? because again, for my understanding, there was a change, even if not triggered by direct user click. Why would I need to trigger it manually?
I'm not sure I understand what you mean. What is happening now, is that whenever you change the all checkbox, the other checkboxes will be checked/unchecked the same as all, and then the change event is triggered.
Because you added a listener for change, that function will then fire. I.e. this function will run:
function() {
// get IDS from all selected checkboxes and make a comma separated string
var ids = checkboxes.filter(':checked').map(function() {
return this.value;
}).get().join(',');
// put IDS inside a text input field
jQuery('#o99-post-visible-ids').val(ids);
// console.log(ids);
}
Without your .trigger("change") (or .change() in short), you only change a property of the inputs. So the object changes, indeed, but that does not mean the change event is triggered. It does sound counter-intuitive, but events are only triggered by user actions or if you call the event explicitly - in no other way do events get triggered.
its because you have written jQuery('#o99-post-visible-ids').val(ids); inside a function which happens only when the change event done on the inputs, assigning prop directly through .prop does not trigger the change event and so the result handler wont run
Now if I understand you correctly...
...because you're giving every check box the same ID? If you wish to apply it to more than a single element, it is best practice to use a class selector instead.
jQuery(".o99-check-all").change(function () {
// without .trigger('change') chained
jQuery(".o99-check-all").prop('checked', jQuery(this).prop("checked"));
});
See link
https://api.jquery.com/change/
Basically I am going with an application style site for mobile view and they wanted a button on the bottom to flip through the pages in order while the nav at the top allows you to flip through them in any order. I want to change the bottom button to link to the following page depending on which one it is on. so I want the button to change links when specific buttons are focused. I tried many different things and can not seem to make it work. Please do your magic.
if(document.getElementById("abt").hasFocus()) {
document.getElementById("golink").href = "#work";
}
I don't believe focus is what you're looking for as focus can be fleeting to track outside of form objects.
I've mocked up a possible solution to help aid you to what you're looking for.
Here is a link to the JSFiddle:
EDIT - Updated Fiddle: https://jsfiddle.net/t0uwff4t/
Javascript in the fiddle:
// Array of sections.
var sections = [
'#first',
'#second',
'#third',
'#fourth',
'#fifth'
]
// Get the last value in the array.
var lastArrayValue = sections[sections.length - 1];
// Button pointer
var btn = document.querySelector('#button');
// On click of the button, do work.
btn.onclick = function() {
// Get the current href.
var href = btn.getAttribute('href');
// Ternary operator setting the index to zero if the user is on the last index of array.
// Else set the index to the next array value.
var index = (href === lastArrayValue) ? 0 : sections.indexOf(href) + 1;
// Set the href attribute.
btn.setAttribute('href', sections[index]);
}
NOTE: While this example works, this is only a mockup - meaning that it doesn't account for rapid button clicking by the user to 'flip pages'.
Good luck and I hope this helps you to your goal.
I have an input box, and a button and other random elements in the HTML document. my question is this: can JavaScript use an id that is entered into the input box and hide the element with that Id when the button is clicked? please let me know how to do this. I am making a program that creates elements based on certain button clicks, so the user can format html without knowing code. I am trying to add dynamic functions and I have absolutely no idea what to do, because the functions need to be flexible enough to use the input in them.
the reason I can't insert the functions directly is because I want the button to use a function that is specifically created to do what the user wants.
I also dont know jquery, inly HTML JavaScript and CSS
Yes, but you are on entirely the wrong track: you don't need to create a new function in response to user input to do that.
You just need to use user input in a function.
function myEventHandler(event) {
var user_input = document.getElementById('my_text_input').value;
var user_selected_element = document.getElementById(user_input);
if (user_selected_element) {
user_selected_element.style.display = "none";
}
}
document.getElementById('the_button').addEventListener('click', myEventHandler);
You'll need to create a function that is passed to the button's click handler. When the button is clicked, we can get the input's value and process it from there. Here's a working fiddle.
function hideById() {
var id = document.getElementById('id').value;
document.getElementById(id).style.display = 'none';
}
document.querySelector("button").addEventListener("click", hideById);