I'm trying to loop through array of objects to display them on a grid based off of input values for my library project. My code for loop is:
const addBook = (ev) => {
ev.preventDefault();
let myLibrary = [];
let bookInfo = {
title: document.getElementById('title').value,
author: document.getElementById('author').value,
pages: document.getElementById('pages').value,
}
myLibrary.push(bookInfo)
for (i = 0; i < myLibrary.length; i++) {
console.log(myLibrary)
var container = document.getElementById('book-shelf')
var div = document.createElement('div')
div.classList.add('cell')
container.appendChild(div);
}
var submitBtn = document.querySelector('.submit-btn');
submitBtn.addEventListener('click', addBook)
Each time I enter title,author, and pages value and click submit button, it goes through and gives me a cell. If try to add another book, it gives me 3 cells rather then 2. Doing another add gives me 6 instead of 3. How can I make it where I can add a book each time one by one rather then it adding multiple times?
There are three main issues.
A missing curly brace after the function and before the button listener.
myLibrary is being redefined each time the function is called which is one of the reasons you're having to look over the data each time. You want to define it outside the function so you can add one book at a time to it when addBook is called.
With myLibrary no longer being redefined each time there's no need for the loop. We can just add the HTML for the book to the book shelf onSubmit.
(Note: In this working example (I've added some HTML for a table, and the inputs and buttons, and created some code to add the new book to the shelf), I've renamed the myLibrary variable to bookShelf to keep things consistent with the HTML naming.)
// Cache all the elements up front
const titleEl = document.getElementById('title');
const authorEl = document.getElementById('author');
const pagesEl = document.getElementById('pages');
const bookshelfEl = document.getElementById('bookshelf');
const submitBtn = document.querySelector('.submit-btn');
// Add the listener
submitBtn.addEventListener('click', addBook, false);
titleEl.focus();
// Our bookShelf variable is now
// outside the function
const bookShelf = [];
function addBook() {
// Because we've cached the elements
// we can now just grab the values from each
const bookInfo = {
title: titleEl.value,
author: authorEl.value,
pages: pagesEl.value,
}
bookShelf.push(bookInfo);
// Once we've added our book we can grab the
// title, author, and pages variables from it
const { title, author, pages } = bookInfo;
// Create a row for the table
const row = document.createElement('tr')
// Create some HTML and add it to the div
row.innerHTML = `
<td>${title}</td>
<td>${author}</td>
<td>${pages}</td>
`;
bookshelfEl.appendChild(row);
}
table { border-collapse: collapse; border: 2px solid #565656; width: 100%; }
td { text-align: center; }
.heading { background-color: #efefef; border-top: solid thin #565656; }
tr { border: solid thin #ababab; }
<input placeholder="Title" id="title" />
<input placeholder="Author" id="author" />
<input placeholder="Pages" id="pages" />
<button type="button" class="submit-btn">Submit</button>
<H3>Book shelf</H3>
<table>
<tbody id="bookshelf">
<tr class="heading">
<td>Title</td>
<td>Author</td>
<td>Pages</td>
</tr>
</tbody>
</table>
Additional documentation
Destructuring assignment
Template/string literals
Your issue is with the loop. Each time you click the submit button, you add a cell for each item in myLibrary in this block:
for (i = 0; i < myLibrary.length; i++) {
console.log(myLibrary)
var container = document.getElementById('book-shelf')
var div = document.createElement('div')
div.classList.add('cell')
container.appendChild(div);
}
I don't think you need to loop through the array each time. Just add the cell directly whenever you are adding bookInfo data to myLibrary, without using for loop e.g
const addBook = (ev) => {
ev.preventDefault();
let myLibrary = [];
let bookInfo = {
title: document.getElementById('title').value,
author: document.getElementById('author').value,
pages: document.getElementById('pages').value,
}
myLibrary.push(bookInfo)
var container = document.getElementById('book-shelf')
var div = document.createElement('div')
div.classList.add('cell')
container.appendChild(div);
}
var submitBtn = document.querySelector('.submit-btn');
submitBtn.addEventListener('click', addBook)
Related
I'm trying to create a to-do list application. I'm using JS to dynamically create list elements on the web page when a user clicks on the submit button along with their user input.
Here's what I have so far:
const inputTXT = document.querySelector('.inputText'); //input element
const submitBUTT = document.querySelector('.submitButton'); //button element
const listITEMS = document.querySelector('.items'); //list element
function createListElement(inputString){
const newListItem = document.createElement("li");
const newEditButton = document.createElement("button");
const newDeleteButton = document.createElement("button");
const listText = document.createTextNode(inputString);
newListItem.appendChild(listText);
const editText = document.createTextNode("Edit");
newEditButton.appendChild(editText);
const deleteText = document.createTextNode("Delete");
newDeleteButton.appendChild(deleteText);
newListItem.appendChild(newEditButton);
newListItem.appendChild(newDeleteButton);
//assign class to each list element for line below:
newDeleteButton.className = "deleteCLASS";
newEditButton.className = "editCLASS";
//delete function:
var deleteButtonArray = document.querySelectorAll(".deleteCLASS");
for(var i=0; i < deleteButtonArray.length ; ++i){
deleteButtonArray[i].onclick = function(){
this.parentNode.remove();
}
}
return newListItem;
}
function addTask(){
listITEMS.classList.remove('hidden');
const ITEM = createListElement(inputTXT.value);
document.getElementsByClassName("items")[0].appendChild(ITEM);
inputTXT.value = ''; //Resets user input string
}
submitBUTT.addEventListener("click", addTask);
*{
margin: 0;
padding: 0;
}
.container{
width: 800px;
margin: 0 auto;
font-family: sans-serif;
}
.main-header{
background: #e7e7e7;
text-align: center;
margin: 15px;
padding: 20px;
}
.inputText{
margin-top: 20px;
}
h1{
font-size: 35px;
}
ul{
list-style: square;
margin-left: 275px;
}
.hidden{
display: none;
}
.itemLIST{
margin-bottom: 5px;
}
.editBUTT{
margin-left: 20px;
}
.deleteBUTT{
margin-left: 4px;
}
<!DOCTYPE html>
<head>
<title> "To-do List" </title>
<link href="style.css" rel="stylesheet"/>
<meta charset = "UTF-8"/>
</head>
<body>
<div class = "container">
<div class = "main-header">
<h1>My To-do List</h1>
<input type="text" placeholder = "Enter Item" class= "inputText" required>
<button class = "submitButton">Submit</button>
</div>
<ul class ="items hidden">
</ul>
</div>
<script src="script.js"></script>
</body>
</html>
This works for the most parts, but I cannot delete the last list element for some reason.
The only debugging I've done to figure out what is happening is print out the "deleteButtonArray" variable, which uses the keyword "querySelectorAll". From this, I found out that when the user hits the submit button, an empty NodeList printed. When the user hits the submit button a second time, only then do we get a NodeList with 1 element. When the user hits the submit button a third time, we get a NodeList with 2 elements.
It looked like the problem had something to do with querySelectorAll not properly updating in time when the user hits the submit button. I replaced it with getElementsByClassName, and still the same issue.
Now, I think the problem has something to do with the way I'm trying to implement the delete function within the createListElement function.
The way I think my code works:
Every time a user hits the submit button, a list element is created along with an array (actually a NodeList) that contains all the list elements present so far. This means that if I delete list elements, the array will update with the correct number of list elements.
And, it does update correctly, for the most parts. I just don't know why the array is empty when we first create an element. Shouldn't querySelectorAll or getElementsByClassName return a non-empty NodeList when the user first hits the submit button?
All right, so here's a solution I've tried and tested it, and it seems to be working.
I removed the delete item portion out of the createListElement function, made it its own function, and added an event listener to each delete button that's created, so it will run the now separate delete function when clicked.
No changes were made to the HTML or the CSS.
const inputTXT = document.querySelector('.inputText'); //input element
const submitBUTT = document.querySelector('.submitButton'); //button element
const listITEMS = document.querySelector('.items'); //list element
function createListElement(inputString){
const newListItem = document.createElement("li");
const newEditButton = document.createElement("button");
const newDeleteButton = document.createElement("button");
const listText = document.createTextNode(inputString);
newListItem.appendChild(listText);
const editText = document.createTextNode("Edit");
newEditButton.appendChild(editText);
const deleteText = document.createTextNode("Delete");
newDeleteButton.appendChild(deleteText);
newListItem.appendChild(newEditButton);
newListItem.appendChild(newDeleteButton);
//Here is the new addEventListener
newDeleteButton.addEventListener("click", deleteStuff);
//assign class to each list element for line below:
newDeleteButton.className = "deleteCLASS";
newEditButton.className = "editCLASS";
return newListItem;
}
//Here is the new delete function. The onclick function that was there before has been removed because, in this case, it's not needed.
function deleteStuff() {
//delete function:
var deleteButtonArray = document.querySelectorAll(".deleteCLASS");
for(var i=0; i < deleteButtonArray.length ; ++i){
this.parentNode.remove();
}
}
function addTask(){
listITEMS.classList.remove('hidden');
const ITEM = createListElement(inputTXT.value);
document.getElementsByClassName("items")[0].appendChild(ITEM);
inputTXT.value = ''; //Resets user input string
}
submitBUTT.addEventListener("click", addTask);
I am a beginner in Javascript. What I am trying to do is when a user clicks on "Click to start loop", the first <li> will be 1. The second time the user clicks it, it will be 2, and the third time, it will be 3. After the third click, the loop will break.
The issue with my code is that it always displays the number 3 instead of starting from 1 and going all the way to 3.
function myFunction() {
demo = document.getElementById("demo")
ul = document.createElement("ul")
demo.appendChild(ul)
li = document.createElement("li")
ul.appendChild(li)
for (let i = 1; i <= 3; i++){
li.innerText = i
}
}
<p id="demo" onclick="myFunction()">Click to start loop</p>
It is because, there is only one 'li' element created before loop starts and at the end of loop, it is just updating the final innterText.
You can fix it by moving li creation code to loop
function myFunction() {
demo = document.getElementById("demo")
ul = document.createElement("ul")
demo.appendChild(ul)
<--- from here
for (let i = 1; i <= 3; i++){
li = document.createElement("li") <--- to here
ul.appendChild(li)
li.innerText = i
}
}
You just have to save the current loop value in some place:
let i = 1;
function myFunction() {
// Check for i value
if (i === 4) return;
demo = document.getElementById("demo")
ul = document.createElement("ul")
demo.appendChild(ul)
li = document.createElement("li")
ul.appendChild(li)
// Update the i value
li.innerText = i++;
}
<p id="demo" onclick="myFunction()">Click to start loop</p>
One solution could be to create a global variable with initial value set to 1 and increase it every time there is a click on your <p> tag.
I have implemented the same using the global variable counter.
<p id="demo" onclick="myFunction()">Click to start loop</p>
<script>
var counter = 1;
function myFunction() {
if(counter === 4){
return;
}
demo = document.getElementById("demo");
ul = document.createElement("ul");
demo.appendChild(ul);
li = document.createElement("li");
ul.appendChild(li);
li.innerText = counter;
counter++;
}
</script>
I think you should put li = document.createElement("li") inside of the loops
index.js
function myFunction(status) {
demo = document.getElementById("demo")
ul = document.createElement("ul")
demo.appendChild(ul)
for (let i=1; i<=3; i++) {
li = document.createElement("li")
li.innerText = i
ul.appendChild(li)
}
}
This is unclear, are you looking for javascript function generator ?
const
ul_List = document.body.appendChild( document.createElement('ul') )
, generator = (function* ()
{
for (let i = 0; (++i < 4);)
{
ul_List.appendChild( document.createElement('li') ).textContent = i
yield
}
})()
<p id="demo" onclick="generator.next()" >Click to 3 times loop</p>
Counters
A function that deals with an incrementing variable (aka counter) usually declares or defines it as a number outside of a loop then iterates the variable with a ++ or += operator within the loop. But would a loop within an event handler that increments a number by +1 per click make much sense? So forget about iterations based on a single run of a function/event handler.
The next problem is that once the function/event handler is done, the counter is garbage collected (deleted from memory), so on the next click it is back to what it was initially (usually 0) -- so you need the user to click a HTML element, increase a number by one, and increase it by one per click thereafter. There a few ways to keep the counter's last value:
HTML/DOM
Store the last value in a HTML form control by [value]
let counter = 0;
counter++;
document.forms[0].count.value = counter;
....
<input id='count' type='hidden' value='0'><!--value will be '1'-->
Store the last value in any other type of HTML element by [data-*]
or text content
document.querySelector('.count').dataset.idx = counter;
....
<nav class='count' data-idx='1'></nav>
document.querySelector('aside').textContent = counter;
....
<aside>2</aside>
Keep in mind any value stored in HTML is converted into a string so when getting the variable counter value you must convert it back into a real number:
parseInt(document.forms[0].count.value);
parseFloat(document.querySelector('.count').dataset.idx);
Number(document.querySelector('aside').textContent);
Closures
A better way IMO is to deal with variable scope. If you noticed in the previous code, let and const are used instead of var. The reason for this is scope.
Function Scope: If var was used, then all variables would be influenced by anything inside or the immediate outside of the function it is in. If completely outside of all functions then it is global (much more susceptible to side effects and buggy behavior).
Block Scope: let and const scope is block which means they can only be accessed within the brackets they are located in:
var G = `any function, method, expression, etc can affect it or be affected
by it`;
function clickHandler(e) {
var L = `vulnerable to anything within the function and immediately
outside the function`;
if (e.target.matches('button')) {
let A,
const B = `only affects or get affected only by things within the
brackets`;
var C = `even buried deep within a function it will be hoisted to be
accessible to everything within this function`;
}
function addItem(node, count) {
/* Although a function defined within another function it cannot access
A or B*/
}
}
Wrapping a function/event handler in another function in order to provide an isolated scope and an environment wherein variables can exist past runtime and avoid garbage collection is a closure. The following examples are closures #1 is simple and #2 is more refined (user can edit each list item directly). Go here for details on scope and closures.
Example 1
function init() {
const ul = document.querySelector('ul');
let i = 0;
function clickHandler(e) {
i++;
addItem(this, i);
}
ul.onclick = clickHandler;
};
function addItem(list, counter) {
const li = document.createElement('li');
list.append(li);
li.textContent = counter;
};
init();
<ul>Click to Add Item</ul>
Example 2
function init() {
const ui = document.forms[0];
let counter = 1;
function addItem(event) {
const clicked = event.target;
if (clicked.matches('legend')) {
const list = clicked.nextElementSibling;
let marker = `${counter++}`.padStart(2, '0');
list.insertAdjacentHTML('beforeEnd', `<li contenteditable data-idx="${marker}"></li>`);
}
}
ui.onclick = addItem;
};
init();
form {
font: 1.5ch/1 Consolas;
}
legend {
font-size: large;
font-weight: bold;
user-select: none;
cursor: pointer;
}
legend::after {
content: 'Click to Add Item';
font-size: small;
}
ul {
list-style: none;
}
li {
margin: 5px 11px 5px 9px;
padding: 4px 8px;
border-bottom: 1px solid #980;
}
li::marker {
display: list-item;
content: attr(data-idx)'.';
margin-bottom: -2px;
}
<form>
<fieldset>
<legend>List<br></legend>
<ul></ul>
</fieldset>
</form>
In this program, I'm able to add inputs with a button but I need to show the length of each input as it changes. I'm able to get the length using an EventListener, but I'm not sure how to change the text value for any newly created buttons.
On line 12, you can see that I'm able to change the value successfully on the first input but I'm using an html variable. If you look at my addCell() function, you'll see that I have an element as a child of each node to keep track of the length of each input. I need to access that element in my change() function so I can set the event.target.value.length to the corresponding nodes child element.
I've tried using this, setting var x = this and I've tried using the event.target properties to find the corresponding node and even innerHTML.
var i = 0;
var count = 1;
var length = 2;
var chars = 0;
document.addEventListener('input', function (evt) {
change(evt);
});
function change(elem) {
var check = document.getElementById("first");
if (event.target == check) {
document.getElementById("len").innerHTML = event.target.value.length;
return;
}
// Here's where I'm stuck
}
function removeCell() {
if (count <= 1) {
alert("Illegal operation, the police have been notified.")
return;
}
var elem = document.getElementById('main');
elem.removeChild(elem.lastChild);
count = count - 1;
length = length - 1;
}
function addCell() {
var node = document.createElement('div');
node.innerHTML += length;
var inp = document.createElement('INPUT');
var size = document.createElement('size');
inp.setAttribute("type", "text");
node.appendChild(inp);
node.appendChild(size);
document.getElementById('main').appendChild(node);
count += 1;
length += 1;
i += 1;
}
#area {
width: 585px;
background-color: lightgrey;
color: black;
border-style: solid;
margin-left: auto;
margin-right: auto;
min-height: 100px;
height: auto
}
#texts {
width: 220px;
height: 50px;
border-style: solid;
}
body {
background-color: grey;
}
<div id="area">
<form id="main">
<pre><b> input </b> length</pre>
<span id="list">
1<input type="text" id="first"> <var id="len"></var>
</span>
</form>
<br />
<button onclick="addCell()">Add Cell</button>
<button onclick="removeCell()">Remove Cell</button>
<button onclick="sort()">Sort</button>
</div>
Since I'm able to use alert() to show me the correct length of each newly created input each time it changes, I know there's a way to access the "size" element I created to update it using event.target.value.length
Your problem is that you use a "global" input event listener and your change() function is not programmed to handle multiple input fields because in it you are querying known element ids first and len.
If you want to go with a global listener you have to tell your change() function how to access the new input and corresponding target fields.
An easier way is that you modify your addCell() function and attach an event listener to the input field that you are creating instead of using a global one. Thereby each input field holds its own event listener. Since both the input field and your size element, which displays the length of the input value, are created in the same scope you can use easily write the length to the corresponding size element.
inp.addEventListener('input', function(){
size.innerText = inp.value.length;
});
If you want this to work with your provided HTML you need to remove your first input field and call addCell() manually so that your initial input gets rendered.
Your code should then look like this (note: I set var count = 0; and var length = 1;):
var i = 0;
var count = 0;
var length = 1;
var chars = 0;
function removeCell() {
if (count <= 1) {
alert("Illegal operation, the police have been notified.")
return;
}
var elem = document.getElementById('main');
elem.removeChild(elem.lastChild);
count = count - 1;
length = length - 1;
}
function addCell() {
var node = document.createElement('div');
node.innerHTML += length;
var inp = document.createElement('INPUT');
var size = document.createElement('size');
inp.setAttribute("type", "text");
inp.addEventListener('input', function(){
size.innerText = inp.value.length;
});
node.appendChild(inp);
node.appendChild(size);
document.getElementById('main').appendChild(node);
count += 1;
length += 1;
i += 1;
}
addCell();
#area {
width: 585px;
background-color: lightgrey;
color: black;
border-style: solid;
margin-left: auto;
margin-right: auto;
min-height: 100px;
height: auto
}
#texts {
width: 220px;
height: 50px;
border-style: solid;
}
body {
background-color: grey;
}
<div id="area">
<form id="main">
<pre><b> input </b> length</pre>
<span id="list"></span>
</form>
<br />
<button onclick="addCell()">Add Cell</button>
<button onclick="removeCell()">Remove Cell</button>
<button onclick="sort()">Sort</button>
</div>
If HTML layout is planned out and is consistent you can use [name] attribute for form controls and .class or even just the tagName. Use of #id when dealing with multiple tags is difficult and unnecessary. Just in case if you weren't aware of this critical rule: #ids must be unique there cannot be any duplicate #ids on the same page. Having duplicate #ids will break JavaScript/jQuery 90% of the time.
To accessing tags by .class, #id, [name], tagName, etc. use document.querySelector() and document.querySelectorAll() for multiple tags.
To access forms and form controls (input, output, select, etc) by [name] or #id use the HTMLFormElement and HTMLFormControlsCollection APIs.
.innerHTML is destructive as it overwrites everything within a tag. .insertAdjacentHTML() is non-destructive and can place an htmlString in 4 different positions in or around a tag.
Event handlers and event listeners work only on tags that were initially on the page as it was loaded. Any tags dynamically added afterwards cannot be registered to listen/handle events. You must delegate events by registering an ancestor tag that's been on the page since it was loaded. This was done with delRow() since the buttons are dynamically created on each row (changed it because one delete button that removes the last row isn't that useful. ex. 7 rows and you need to delete 4 rows just to get to the third row).
Here's a breakdown of: [...ui.len] ui references all form controls .len is all tags with the [name=len]. The brackets and spread operator converts the collection of len tags to an array.
There's no such thing as <size></size>. So document.createElement('size') is very wrong.
const main = document.forms.main;
main.oninput = count;
main.elements.add.onclick = addRow;
document.querySelector('tbody').onclick = delRow;
function count(e) {
const active = e.target;
const ui = e.currentTarget.elements;
const row = active.closest('tr');
const idx = [...row.parentElement.children].indexOf(row);
const length = [...ui.len][idx];
length.value = active.value.length;
return false;
}
function addRow(e) {
const tbody = document.querySelector('tbody');
let last = tbody.childElementCount+1;
tbody.insertAdjacentHTML('beforeend', `<tr><td data-idx='${last}'><input name='txt' type="text"></td><td><output name='len'>0</output></td><td><button class='del' type='button'>Delete</button></td>`);
return false;
}
function delRow(e) {
if (e.target.matches('.del')) {
const row = e.target.closest('tr');
let rows = [...row.parentElement.children];
let qty = rows.length;
let idx = rows.indexOf(row);
for (let i = idx; i < qty; i++) {
rows[i].querySelector('td').dataset.idx = i;
}
row.remove();
}
return false;
}
body {
background-color: grey;
}
#main {
width: 585px;
background-color: lightgrey;
color: black;
border-style: solid;
margin-left: auto;
margin-right: auto;
min-height: 100px;
height: auto
}
tbody tr td:first-of-type::before {
content: attr(data-idx)' ';
}
<form id="main">
<table>
<thead>
<tr>
<th class='txt'>input</th>
<th class='len'>length</th>
<th><button id='add' type='button'>Add</button></th>
</tr>
</thead>
<tbody>
<tr>
<td data-idx='1'><input name='txt' type="text"></td>
<td><output name='len'>0</output></td>
<td><button class='del' type='button'>Delete</button></td>
</tr>
</tbody>
</table>
<!--These are dummy nodes because of the
HTMLFormControlsCollection API ability to use id or name, there
must be at least 2 tags with the same name in order for it to
be considered iterable-->
<input name='txt' type='hidden'>
<input name='len' type='hidden'>
</form>
when I assign the
// psuedo code
.innerHTML = [ a button here ]; // see exact code below
it does nothing when clicked. Is there an alternative to doing this? or is it not possible?
the table is dynamically created by JavaScript. As each cell is populated, a criterion is checked - and if met, will produce a button in the following cell. this works and does as intended, however, the button does nothing when clicked.
When checking the source, the button does not show, even though it is there on the HTML page.
I tried elements, that did not work, and I think it is because it is handled outside of the table, where this code is internal.
// the culprit line
cell = row.insertCell(-1); cell.innerHTML = '<button class="calc" onclick="calculateGrade();"> + </button>';
Not sure if I need to post more code, but this line should show what I am attempting to do.
the button has to be done dynamically since it checks each row.
For educational purposes:
why is the button not able to respond?
is there a way to make this work as is?
is there another option to achieve the same results?
edit ----------------------------------
Just a snippit of the function it calls. code ommited as it never gets there
function calculateGrade() {
console.log('here'); // <--- THIS NEVER SHOWS UP, BUTTON IS NOT SENDING IT HERE
// code cleared for clarity
}
EDIT 2
for(let i=0; i < studentList.length-1; i++) {
row = table.insertRow(-1);
var
cell = row.insertCell(-1); cell.innerHTML = studentList[i];
cell = row.insertCell(-1); cell.innerHTML = grade
cell = row.insertCell(-1); cell.innerHTML = maxGrade;
cell = row.insertCell(-1); cell.innerHTML = student.name;
if(student.grade < maxGrade) { // change later to within 90%
cell = row.insertCell(-1); cell.innerHTML = '<button onclick="calculateGrade();" > + </button>';
}else{
cell = row.insertCell(-1); cell.innerHTML = 'Pass';
}
}
manipulation the dom
if you want to manipulate the dom "a hundrets of time" you should use a framework like jQuery or the javascript own functions for dom manipulation instead of innerHTML.
Why?
HTML has changed from time to time and you can't know, if your innerHTML will be correct (formatted) in the future.
document.createElementas example will work in every browser.
Also there is the possibility, that dom manipulation functions will be more optimized by compilers and engines in future.
Dynamic listeners
If you create dom elements dynamicly you don't have to declare the listeners as text.
let button = document.createElement('button');
button.addEventListener('onclick', calculateGrade);
cell.appendChild(button);
also this is less vulnerable for typos ;)
dynamic element creation and reflow
Whenever you change something in the dom that is a visible change, the browser fill fire a reflow. It will recalculate all elements sizes. As you can imagine, adding "a hundrets" of buttons will cause a hundrets of reflows.
Reflow will not trigger on invisible objects. so best practise if you manipulate a lot of elements is to turn the parent invisible, then make your changes and make the parent visible again. In this case you trigger a reflow only twice.
function myFunction() {
var row = document.getElementById("myRow");
var x = row.insertCell(-1);
x.innerHTML = '<button class="calc" onclick="calculateGrade();"> +button </button>';
}
function calculateGrade() {
alert("Hi am working action");
}
Check here the new add button is working when I am adding cell.innerHTML there will calc button and when you will click it will show "Hi am working action," I think it will help you
<!DOCTYPE html>
<html>
<head>
<style>
table, td {
border: 1px solid black;
}
</style>
</head>
<body>
<p>Click the button to insert new cell(s) at the beginning of the table row.</p>
<table>
<tr>
<td>First cell</td>
<td>Second cell</td>
<td>Third cell</td>
</tr>
<tr id="myRow">
<td>First cell</td>
<td>Second cell</td>
<td>Third cell</td>
</tr>
<tr>
<td>First cell</td>
<td>Second cell</td>
<td>Third cell</td>
</tr>
</table><br>
<button onclick="myFunction()">Try it</button>
</body>
</html>
Do not use onevent attributes:
<button onclick="func()">...</button>
Use either onevent property:
document.querySelector('button').onclick = function;
Or an eventListener:
document.querySelector('button').addEventListener('click', function)
Use Event Delegation -- a programming pattern that allows us to register a single event handler to listen for an unlimited number of elements. Here's a simplified outline:
Choose an element that is a common ancestor to all of the elements we want to react to an event. This includes window and document as well, but window should be used for key events (ex. document.onclick = calculateGrade)
The callback function (ex. calculateGrade(e)) must pass the Event Object (event, evt, or e are the most common names).
Reference the clicked, changed, hovered, etc. element with the event property Event.target (ex. const tgt = e.target).
Isolate Event.target by if...else control statements (ex. if (tgt.matches('button')) {...)
The rest of the code is just to dynamically generate a table for demonstration purposes. Although the code that generates a table in OP question is dubious it wasn't part of the question so there's no explanation for that part in the demo.
const tData = [{
name: 'zer00ne',
grade: 100
}, {
name: 'Trump',
grade: 25
}, {
name: 'Joe Average',
grade: 75
}, {
name: 'Slacker',
grade: 50
}];
const tHdrs = [`<th>#</th>`, `<th>Name</th>`, `<th>Grade</th>`, `<th> </th>`];
const frag = document.createDocumentFragment();
const table = document.createElement('table');
frag.appendChild(table);
const title = table.createCaption();
title.textContent = 'Student Grades';
const head = table.createTHead();
const hRow = head.insertRow(-1);
let h = tHdrs.length - 1;
for (let th of tHdrs) {
th = hRow.insertCell(0).outerHTML = tHdrs[h];
--h;
}
let rowQty = tData.length;
let colQty = tHdrs.length;
const body = document.createElement('tbody');
table.appendChild(body);
for (let r = 0; r < rowQty; r++) {
let row = body.insertRow(-1);
for (let c = 0; c < colQty; c++) {
let col = c => row.insertCell(c);
let cell = col(c);
if (c === 0) {
cell.textContent = r + 1;
} else if (c === colQty - 1) {
if (tData[r].grade > 69) {
cell.textContent = 'Pass';
} else {
cell.innerHTML = `<button>Calculate</button>`;
}
} else if (c === 1) {
cell.textContent = tData[r].name;
} else {
cell.textContent = tData[r].grade;
}
}
}
const foot = table.createTFoot();
const fRow = foot.insertRow(-1);
const fCol = fRow.insertCell(0);
fCol.textContent = 'Passing grade is 70% or more.'
fCol.setAttribute('colspan', '4');
fCol.className = 'note';
document.body.appendChild(frag);
// Relevant Code
document.onclick = calculateGrade;
function calculateGrade(e) {
const tgt = e.target;
if (tgt.matches('button')) {
let row = tgt.closest('tr');
let grd = row.children[2].textContent;
let num = parseFloat(grd);
console.log(num);
}
return false;
}
:root {
font: 400 5vh/1 Verdana;
}
table {
table-layout: fixed;
border-collapse: collapse;
width: 96%;
}
caption {
font-weight: 700;
font-size: 1.2rem;
}
th,
td {
border: 2px ridge #000;
min-height: 30px;
}
th:first-of-type {
width: 10%;
}
th:nth-of-type(3) {
width: 20%;
}
th:last-of-type {
width: 25%
}
td:first-of-type,
td:nth-of-type(3) {
text-align: center;
font-size: 1.2rem;
font-family: Consolas;
}
td:nth-of-type(3)::after {
content: '%';
}
td:first-of-type.note {
text-align: right;
font-size: 0.85rem;
font-family: Verdana;
border: none
}
button {
font: inherit;
width: 100%
}
I want to create a to-do list with an array, functions and js must be separated from HTML. The HTML part is fine, but I have troubles finishing the functions.
Also the EventListener works.
<form id="todoForm">
<input id="todoInput" type="text">
<button type="button" id="button">Add your To Do</button>
</form>
<ol id="toDoList"></ol>
We have the array 'todos', the button click is noticed, and the function 'addTodo' is called, which (I hope so) pushes the input into the array.
I don't know how to call next function, which should create the array items as li elements, that is the place where I need help. The content in function 'printTodos' is garbage.
var todos = [];
document.getElementById('button').addEventListener('click', function
addTodo () {
todos.push('input')
function printTodos () {
var item = document.createElement("li");
var node = createTextNode(input);
// I am stuck
}
});
You are very close, except you do not want to create the addTodo and printTodos function inside your click event listener.
What you can instead do is define the two functions outside of it, and call them inside your click event listener, like so:
var todos = [];
function addTodo() {
var inputValue = document.getElementById('todoInput').value;
todos.push(inputValue);
}
function printTodos() {
var list = document.getElementById('toDoList');
list.innerHTML = ''; //Reset the list content whenever we print the todos, so we don't end up with duplicate todo items.
for (var i = 0; i < todos.length; i++) {
var li = document.createElement('li');
var listItem = li.appendChild(document.createTextNode(todos[i]));
list.appendChild(listItem);
}
}
document.getElementById('click', function() {
addTodo();
printTodos();
});
What we do here is, in the addTodo function, we programmatically get the text typed into the todoInput and add it to the array. Then, in the printTodos function, we loop over all the entered todos and create <li> element filled with the todo text. At the end, we append the new list item to the toDosList.
I've made a fully working code pen example for you. Please take a look on it or go with the above answer. Here is what you need https://codepen.io/waleedbinkhalid/pen/aRvwmo
var todos = [];
document.getElementById('button').addEventListener('click', function () {
var list = $('#toDoList');
var todoInput = $('#todoInput').val();
todos.push(todoInput)
var item = document.createElement("li");
for (var i = 0; i < todos.length; i++) {
var listItem = $(document.createTextNode(todos[i]));
list.append(document.createTextNode(todos[i]));
}
});
`