I'm am having a issue where I have a click event.
This is the code:
for (const button of itemClick) button.addEventListener("click", function() {
clickCount++;
if (clickCount == 1) {
value1 = button.getAttribute("value");
}
if (clickCount >= 2) {
value2 = button.getAttribute("value");
clickCount = 0;
onItemClick();
}
});
itemClick refers to a document classname variable called item
So if I click twice on that item it should add a second item which works but clicking on that item created by JavaScript doesn't want to work so I have to somehow let JavaScript know the HTML has changes so when I click on it it also has effect but I don't know how to do that and can't find any information for it.
I am adding the element in HTML like this:
let itemDiv = document.createElement("div");
let imageDiv = document.createElement("div");
let imgEl = document.createElement("img");
let itHeading = document.createElement("h6");
let itemslistcontent = document.getElementById("items");
itemslistcontent.appendChild(itemDiv);
itemDiv.appendChild(imageDiv);
imageDiv.appendChild(imgEl);
itemDiv.appendChild(itHeading);
itemDiv.classList.add("item");
itemDiv.setAttribute("value", prop);
itHeading.innerHTML = prop;
This is the full function:
function onItemClick() {
for (var prop in itemNames) {
if (itemNames[prop].includes(value1) && itemNames[prop].includes(value2)) {
value1 = "";
value2 = "";
let itemDiv = document.createElement("div");
let imageDiv = document.createElement("div");
let imgEl = document.createElement("img");
let itHeading = document.createElement("h6");
let itemslistcontent = document.getElementById("items");
itemslistcontent.appendChild(itemDiv);
itemDiv.appendChild(imageDiv);
imageDiv.appendChild(imgEl);
itemDiv.appendChild(itHeading);
itemDiv.classList.add("item");
itemDiv.setAttribute("value", prop);
itHeading.innerHTML = prop;
console.log(itemslistcontent);
} else {
value1 = "";
value2 = "";
}
}
}
When you add new elements to the DOM programmatically, those elements are called dynamic.
Since your code is already run when the dynamic elements are added to the DOM, the event listeners are only registered to the elements which were already present at the time of code execution.
Thus, you need to add the event listener again to the dynamic component.
Also, why don't you make use of the dblclick event in JavaScript?
Try the following code, its adding the even listener as and when the dynamic elements are created.
function onItemClick() {
for (var prop in itemNames) {
if (itemNames[prop].includes(value1) && itemNames[prop].includes(value2)) {
value1 = "";
value2 = "";
let itemDiv = document.createElement("div");
let imageDiv = document.createElement("div");
let imgEl = document.createElement("img");
let itHeading = document.createElement("h6");
let itemslistcontent = document.getElementById("items");
itemslistcontent.appendChild(itemDiv);
itemDiv.appendChild(imageDiv);
imageDiv.appendChild(imgEl);
itemDiv.appendChild(itHeading);
itemDiv.classList.add("item");
itemDiv.setAttribute("value", prop);
itHeading.innerHTML = prop;
console.log(itemslistcontent);
// adding the event listener
itemDiv.addEventListener("dblclick", function() {
value2 = button.getAttribute("value");
clickCount = 0;
onItemClick();
});
} else {
value1 = "";
value2 = "";
}
}
}
Event Delegation
The easiest way to track any event (ie "click") on any element of any amount, whether dynamically added or not, is to use Event Delegation.
Register an element that will or is containing all of the buttons we want to monitor/control by way of the "click" event.
// Event Property
document.querySelector('main').onclick = clickHandler;
OR
// EventListener
document.querySelector('main').addEventListener('click', clickHandler);
Whenever this parent element is clicked function clickHandler() will run. This event handler (just a more descriptive name for a function triggered by an event) will delegate the event to the exact button the user clicked by:
using the event.target property to reference the element the user clicked.
narrow it down by the use of if/if else/else conditions, and the .matches() method.
if (event.target.matches('button')) {...
Advantages
We only need to register events to a single parent element. window and document could be used but it's better to use an element further down like <main> or <form> IMO. This parent element can be referenced using event.currentTarget property.
const listener = event.currentTarget;
console.log(listener.tagName); // MAIN
Any descendant element of the event.currentTarget can be selected. If the user clicked an element (ie <button>) then it can be referenced directly with the event.target property. If the desired element isn't event.target, but it is it's proximity, we can reference it indirectly many ways.
<main>
<section class='group'>
<figure class='item'>
<img src='pix.jpg'>
<figcaption>
<input>
<button>X</button>
<label>XXXX</label>
...
const clicked = event.target;
if (clicked.matches('button')) {
let group = clicked.closest('.group');
let item = clicked.closest('.item');
let tag = clicked.nextElementSibling;
let txt = clicked.previousElementSibling;
...
/*
Reference to the <section>, <figure>, <input>,
and <label> and finding the exact element
clicked by user
*/
This delegation also includes any dynamically added elements as well. A common mistake newbies make is expecting elements added to the DOM are clickable, but aren't because they need to be registered to the event (ie .onclick or .addEventListener('click')). This is never a concern using event delegation. Add whatever and whenever to the event.currentTarget; and nothing more.
Demo
const main = document.querySelector('main');
main.onclick = clickHandler;
function clickHandler(event) {
const listener = event.currentTarget;
const clicked = event.target;
let grpIdx = indexNodes('.group');
let itmIdx = indexNodes('.item');
const itemHTMLString = `
<figure class='item'>
<img src="https://placeimg.com/100/100/any">
<figcaption>
<b>Item ${itmIdx+1}</b><button>💀</button>
<output class='count'>0</output>
</figcaption>
</figure>`;
const groupHTMLString = `
<section class='group'>
<fieldset>
<legend>Group ${grpIdx+1} Total Clicks</legend>
<output class='total'>1</output>
</fieldset>
<figure class='item'>
<img src="https://placeimg.com/100/100/any">
<figcaption>
<b>Item ${itmIdx+1}</b><button>💀</button>
<output class='count'>1</output>
</figcaption>
</figure>
</section>`;
let item, group, count, total;
if (clicked.matches('button')) {
item = clicked.closest('.item');
group = clicked.closest('.group');
count = clicked.nextElementSibling;
total = group.querySelector('.total');
let cnt = parseInt(count.textContent, 10);
let ttl = parseInt(total.textContent, 10);
if (ttl < 3) {
total.textContent = ttl + 1;
count.textContent = cnt + 1;
group.insertAdjacentHTML('beforeend', itemHTMLString);
indexNodes('.group');
indexNodes('.item');
} else if (ttl === 3) {
let buttons = group.querySelectorAll('button');
for (let btn of buttons) {
btn.disabled = true;
}
listener.insertAdjacentHTML('beforeend', groupHTMLString);
indexNodes('.group');
indexNodes('.item');
} else {
return false;
}
} else {
return false;
}
}
function indexNodes(selector) {
const nodes = document.querySelectorAll(selector);
nodes.forEach((node, index) => node.dataset.idx = index);
return nodes.length;
}
* {
margin: 0;
padding: 0;
}
main {
margin: 10px auto;
padding: 8px;
}
.group {
display: flex;
flex-flow: row nowrap;
justify-content: flex-start;
margin: 8px auto;
padding: 4px;
}
fieldset {
margin-top: -20px
}
legend {
font-size: large;
font-weight: bold;
}
figcaption {
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
}
button,
b,
output {
display: block;
font-size: large;
}
b {
text-align: left;
padding: 8px 8px 0 8px;
font-size: small;
}
.count {
padding: 8px 8px 0 8px;
}
.total {
margin: 0 auto;
text-align: center;
}
button {
cursor: pointer;
max-height: 28px;
}
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
</head>
<body>
<main>
<section class='group' data-idx='0'>
<fieldset>
<legend>Group 1 Total Clicks</legend>
<output class='total'>1</output>
</fieldset>
<figure class='item' data-idx='0'>
<img src="https://placeimg.com/100/100/any">
<figcaption>
<b>Item 1</b><button>💀</button>
<output class='count'>1</output>
</figcaption>
</figure>
</section>
</main>
</body>
</html>
Related
I'm new with HTML & JS and I face the following problem:
I have an input in html that creates a new li Element (in combination with JS); is it possible to give every newly-created li element its own id? For example to delete an specific element?
For Example:
<li id="one"> .. </li>
<li id="two"> .. </li>
So far it creates only <li> ... </li>
I think it can be done with a for loop, but I have no idea how to use it in my case.
See my JS code below:
function NewEntry() {
var Inputfield = document.getElementById("Inputfield");
var AddButton = document.getElementById("AddButton");
var ul = document.querySelector("ul");
var li = document.createElement("li");
li.appendChild(document.createTextNode(Input.value));
ul.appendChild(li);
Input.value = "";
I tried to insert a for loop into my code, but after that it doesn't add any elements.
function NewEntry() {
var Inputfield = document.getElementById("Inputfield");
var AddButton = document.getElementById("AddButton");
var ul = document.querySelector("ul");
var li = document.createElement("li");
for (var i = 0; i < li.length; i++)
li[i].id = 'abc-' + i;
li.appendChild(document.createTextNode(Input.value));
ul.appendChild(li);
Input.value = "";
Your for loop needs curly braces to work properly:
function NewEntry() {
var Inputfield = document.getElementById("Inputfield");
var AddButton = document.getElementById("AddButton");
var ul = document.querySelector("ul");
var li = document.createElement("li");
for (var i = 0; i < li.length; i++) {
abcElements[i].id = 'abc-' + i;
li.appendChild(document.createTextNode(Inputfield.value));
ul.appendChild(li);
}
Inputfield.value = "";
}
Otherwise only the immediate line after the for statement will run as part of the loop.
There also appeared to be a typo - you had Input instead of Inputfield? But I notice there are some other variables used here which are not defined, so I assume some extra code was omitted?
You could count the number of elements inside the <ul> and use that as id for the <li>:
var AddButton = document.getElementById("AddButton");
AddButton.addEventListener("click", NewEntry);
function NewEntry() {
var Inputfield = document.getElementById("Inputfield");
var ul = document.querySelector("ul");
var li = document.createElement("li");
li.id = ul.childElementCount;
li.appendChild(document.createTextNode(Inputfield.value));
ul.appendChild(li);
Inputfield.value = "";
console.log(li);
}
<input type="text" id="Inputfield" />
<button id="AddButton">Add</button>
<ul></ul>
One simple means by which this could be accomplished is as follows, with explanatory comments in the JavaScript:
// a generator function, to generate a constantly increasing counter:
const generator = function*() {
// the initial value of the counter:
let i = 0;
// this loop causes the generator to return a number infinitely
// though while the loop is infinite it yields one value and then
// pauses until the next call; this is one instance where an
// infinite loop is deliberate and not a problem:
while (true) {
// increments the value of i, and then returns it to the
// calling context:
yield ++i;
}
},
// assigning a reference to the generator:
counter = generator(),
// a simple Arrow function that takes two arguments:
// tag: String, the element-type to create, and
// props: an Object of element-properties and values to
// assign to the created element:
create = (tag, props) => Object.assign(document.createElement(tag), props),
// a reference to the various elements to which event-listeners are attached:
list = document.querySelector('#list'),
form = document.querySelector('form'),
input = document.querySelector('input'),
button = document.querySelector('#add'),
// the addNew function, written as an Arrow function:
addNew = () => {
// caching the source of the text:
const inputField = document.querySelector('input');
// here we take the value, trim it to remove leading and trailing
// white-space, and then retrieve its length; if there is a zero
// length (so either nothing was entered, or only whitespace) then
// we return here:
if (inputField.value.trim().length === 0) {
return false;
}
// otherwise we cache a reference to the element to which the
// created <li> element will be appended:
const ul = document.querySelector('#list'),
// we call the create() function, here creating an <li>
// element, and passing in an object of properties and
// values:
li = create('li', {
// we set the 'id' - using a template literal string - to
// the string of 'abc-' plus next-value of the counter:
id: `abc-${counter.next().value}`,
// and set the text-content of the element to the
// trimmed value of the <input>:
textContent: inputField.value.trim()
}),
// here we create a <button>,
deleteButton = create('button', {
// with its className set to 'delete':
className: 'delete',
// its text-content set to 'delete task':
textContent: 'delete task',
// and its type set to 'button' in order to
// prevent the <button> submitting the <form>:
type: 'button'
});
// we append the created <button> to the created <li>:
li.append(deleteButton);
// we append the <li> to the <ul>:
ul.append(li);
// and reset the value of the <input> to an empty string:
inputField.value = '';
};
// here we prevent form submission:
form.addEventListener('submit', (e) => e.preventDefault());
// here we bind an anonymous function as the event-handler of the
// 'keyup' event on the <input> element:
input.addEventListener('keyup', (evt) => {
// if the, or an, 'Enter' key is pressed (note that evt.keyCode is
// almost entirely deprecated now, but is here for backwards
// compatibility, it can and possibly should be removed):
if (evt.key === 'Enter' || evt.keyCode === 13) {
// here we call the addNew() function:
addNew();
}
});
// we bind the addNew() function - note the deliberate lack of
// parentheses - as the event-handler for the 'click' event:
button.addEventListener('click', addNew);
// because we're adding the delete <button> elements we delegate the
// event-handling to the closest ancestor element present in the DOM
// on page-load, here that's the #list element. Here we bind the
// anonymous function as the event-handler for the 'click' event
// which bubbles to the <ul>:
list.addEventListener('click', (evt) => {
// if the element on which the click event fired has an ancestor
// .delete element (therefore it was fired on, or in, the .delete
// <button>):
if (evt.target.closest('.delete')) {
// we navigate from the event-target to the closest <li>
// ancestor element and use Element.remove() to remove that
// element from the document:
evt.target.closest('li').remove();
}
})
*,
::before,
::after {
box-sizing: border-box;
font-family: system-ui;
font-size: 16px;
margin: 0;
padding: 0;
}
main {
inline-size: clamp(15em, 80vw, 1000px);
margin-inline: auto;
}
h2 {
font-size: 1.4em;
text-align: center;
}
fieldset {
display: grid;
padding: 0.5em;
}
legend {
border-inline: 1px solid currentColor;
border-radius: 0.25em;
margin-inline: 1em;
padding-inline: 0.5em;
}
label {
display: flex;
gap: 1em;
justify-content: space-between;
margin-block: 0.5em;
}
label input[type=text] {
flex-grow: 1;
}
#list {
border: 1px solid currentColor;
display: grid;
gap: 0.5em;
list-style-type: none;
margin-block: 1em;
margin-inline: auto;
padding: 0.5em;
}
#list:empty::before {
color: hsl(0deg 30% 30% / 0.8);
content: "A beautiful, quiet day of reflection...";
font-style: italic;
}
#list li {
background-color: lavender;
display: grid;
padding: 0.25em;
}
#list li:nth-child(odd) {
background-color: lightcyan;
}
li[id]::before {
content: "#" attr(id) ": ";
}
.delete {
justify-self: end;
}
<main>
<section>
<h2>To do list</h2>
<form action="#" method="post">
<fieldset>
<legend>New task</legend>
<label>
<span class="labelText">What do you want to do today?</span>
<input type="text">
</label>
<button id="add" type="button">Add task to list</button>
</fieldset>
</form>
<ul id="list"></ul>
</section>
</main>
JS Fiddle demo.
It's worth noting that there is one potential issue using a generator function to generate a counter for an id property; the counter will only ever increase, so if you have three elements abc-1, abc-2, abc-3 and then delete those, and create another three elements those elements will continue from abc-4. So the id will be independent of the number of created elements. This may be a benefit in that a duplicate value can't be created, so there's little chance of a duplicate id being created by a generator.
References:
Arrow functions.
document.createElement().
document.querySelector().
Element.append().
Element.remove().
EventTarget.addEventListener().
Generator functions (function*(){...}).
Object.assign().
String.prototype.trim().
Template literals.
while loop.
I am new to programming and this is my first question. The problem I am having is I am trying to use DOM manipulation on all the child nodes of an html collection. I am expecting the nodes to change background color when they are hovered. Here is what I have tried so far:
let x = 0;
do{
const square = document.createElement("div");
square.className = "squares";
square.setAttribute("id","block");
document.getElementById("container").appendChild(square);
x++;
}
while(x < 16);
var container = document.getElementById("container");
var cells = container.childNodes;
cells.forEach(function(){
cells.onmouseover = function(){
cells.style.backgroundColor = "black";
}
});
console.log(`${cells.length}`);
This doesn't work even though console.log shows 16 child nodes being targeted.
var container = document.getElementById("container");
var cells = container.children[0];
cells.onmouseover = function(){
cells.style.backgroundColor = "black";
}
I have tried this and can use index but of course only that cell will change bg color. I want any cell that is hovered to change also.
I am at a loss for what I am doing wrong here. If anyone can point me in the right direction I would greatly appreciate it.
Welcome to Stack Overflow.
There is an issue in your forEach cycle. Consider the following:
cells.forEach(cell => {
cell.onmouseover = () => {
cell.style.backgroundColor = "black"
}
})
Note that you need to refer to cycle variable instead of the cells array.
Instead of attaching listeners to all the squares you can use event delegation and just have one listener on the container that captures the events from its children as they "bubble up" the DOM.
// Cache the container element, and add a listener to it
const container = document.querySelector('.container');
container.addEventListener('mouseover', handleMouse);
// Create some squares HTML by pushing template
// strings into an array
const html = [];
for (let i = 1; i < 10; i++) {
html.push(`<div class="square">${i}</div>`);
}
// Add that HTML to the container making sure
// we join the array of strings into one string
container.innerHTML = html.join('');
// When a event is fired check that it was
// was from an element with a square class
// and then add an active class to it
function handleMouse(e) {
if (e.target.matches('.square')) {
e.target.classList.add('active');
}
}
.container { display: grid; grid-template-columns: repeat(3, 50px); grid-gap: 0.2em; }
.square { font-size: 1.2em; padding: 0.7em 0.2em; background-color: #565656; color: white; text-align: center; }
.square.active { background-color: thistle; color: black; cursor: pointer; }
<div class="container"></div>
Additional documentation
Template/string literals
I'm doing a tutorial about Events and I'm stuck with Event.StopPropagation() method. It seems that with my example that the bubbling effect to the children are being affected.
Definition of StopPropagation:
The stopPropagation() method prevents propagation of the same event from being called.
Propagation means bubbling up to parent elements or capturing down to child elements.
At first I thought it was a browser problem but it was not the case. I can't find a solution about this.
Code:
// Event Bubbling and Propagation
// element.addEventListener( type, func, useCapture);
let m = document.getElementById('m');
let d = document.getElementById('d');
let p = document.getElementById('p');
let s = document.getElementById('s');
let highlight = (ev)=>{
//add CSS class "gold" to the clicked element
ev.stopPropagation();
let target = ev.currentTarget;
target.className = 'gold';
reset(target);
}
let reset = (_element)=>{
setTimeout(()=>{
_element.className = '';
}, 2000);
}
d.addEventListener('click', (ev)=>{
ev.stopImmediatePropagation();
log('Hi I\'m a DIV');
});
[m,d,p,s].forEach((element)=>{
element.addEventListener('click', highlight);
})
#m,#d,#p,#s{
border: 2px solid black;
padding: 15px;
margin: 10px;
}
.gold{
background-color: gold;
}
<main id="m"> m
<div id="d"> d
<p id="p"> p
<span id="s"> s</span>
</p>
</div>
</main>
This is not a problem with events, you are overthinking this.
You example is simply applying background color to top element, and as children do not have it defined, its applying the top one below it.
And if you remove stopImmediatePropagation() color will be applied as definition is: execute the first event handler, and stop the rest of the event handlers from being executed, and the first one was simply log().
In example below, if you apply background color to child elements, you will see they will stay the same. The color it self is applied only on clicked one.
That means the JS event itself did not bubble up to parent elements or capture down. And class was added only on clicked one. Check it with dev tools or add DOM change event listener on every element.
You confused CSS styling with JS event bubbling.
Example:
// Event Bubbling and Propagation
// element.addEventListener( type, func, useCapture);
let m = document.getElementById('m');
let d = document.getElementById('d');
let p = document.getElementById('p');
let s = document.getElementById('s');
let log = console.log;
let highlight = (ev)=>{
//add CSS class "gold" to the clicked element
ev.stopPropagation();
let target = ev.currentTarget;
target.className = 'gold';
reset(target);
}
let reset = (_element)=>{
setTimeout(()=>{
_element.className = '';
}, 2000);
}
d.addEventListener('click', (ev)=>{
//ev.stopImmediatePropagation();
log('Hi I\'m a DIV');
});
[m,d,p,s].forEach((element)=>{
element.addEventListener('click', highlight);
})
#m,#d,#p,#s{
border: 2px solid black;
padding: 15px;
margin: 10px;
}
#d {
background-color: blue;
}
#s {
background-color: red;
}
.gold{
background-color: gold !important;
}
<main id="m"> m
<div id="d"> d
<p id="p"> p
<span id="s"> s</span>
</p>
</div>
</main>
EDIT:
In your example child nodes do not have background property (rgba(0, 0, 0, 0)), that means you need to set it on click, so that background color of clicked element wont apply on its children.
In code below, you can read the CSS background value of parent element of clicked one, and read its background property. That you take that and apply on all children of clicked one.
This will work in your example.
You can also make sure to apply white to elements if parent was transparent.
Here is a fiddle to play with:
but make sure you will un-comment m (parent) background color to see side effects. You would need to adjust this to suit your production needs
const style = getComputedStyle(target.parentNode);
const backgroundColor = style.backgroundColor;
console.clear();
console.log(backgroundColor);
[...target.children].forEach(el => {
if (backgroundColor==="rgba(0, 0, 0, 0)") {
el.style.backgroundColor = "white";
}else{
el.style.backgroundColor = backgroundColor;}
})
// Event Bubbling and Propagation
// element.addEventListener( type, func, useCapture);
let m = document.getElementById('m');
let d = document.getElementById('d');
let p = document.getElementById('p');
let s = document.getElementById('s');
let log = console.log;
let highlight = (ev)=>{
//add CSS class "gold" to the clicked element
ev.stopPropagation();
let target = ev.currentTarget;
target.className = 'gold';
const style = getComputedStyle(target.parentNode);
const backgroundColor = style.backgroundColor;
console.clear();
console.log(backgroundColor);
[...target.children].forEach(el => {
if (backgroundColor==="rgba(0, 0, 0, 0)") {
el.style.backgroundColor = "white";
}else{
el.style.backgroundColor = backgroundColor;}
})
reset(target);
}
let reset = (_element)=>{
setTimeout(()=>{
_element.className = '';
}, 2000);
}
[m,d,p,s].forEach((element)=>{
element.addEventListener('click', highlight);
})
#m,#d,#p,#s{
border: 2px solid black;
padding: 15px;
margin: 10px;
}
#m {
background-color: blue;
}
.gold{
background-color: gold !important;
}
<main id="m"> m
<div id="d"> d
<p id="p"> p
<span id="s"> s</span>
</p>
</div>
</main>
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>
For a project, I am building an app that gets user input, stores it in an array, and displays the input in the DOM. I did the first two parts but I am having trouble displaying it. More specifically, I can't get the CSS to show up.
I have tried .createElement() which creates a new list-item but it does not include CSS. I am starting to think I am completely going about this incorrectly. If you need more information or code let me know.
\\HTML
<div id="boxhold">
<ul>
<li>
<div class="twogrid">
<h1>Fruit Juice</h1>
<p>50</p>
</div>
</li>
</ul>
</div>
\\CSS
#boxhold {
margin: 0 auto;
ul {
list-style-type: none;
padding: 0;
li {
margin: 0 auto;
width: 408px;
height: 75px;
border: 3px solid $prime-color;
h1 {
font-family: $header-font;
font-weight: $header-weight;
font-size: 1em;
}
p {
font-family: $header-font;
font-weight: $header-weight;
}
}
}
}
\\JS
//Get Data
//Empty array for storing
var added = [];
//Get Data
var userInput = function() {
return {
name: document.getElementById('name').value,
amount: document.getElementById('amount').value
}
};
// Store Data
var newSugar = function(){
return added.push(userInput());
}
// New HTML
function newBox() {
var newLi = document.createElement('li');
var newName = document.getElementById('name').value;
var n = document.createTextNode(newName);
newLi.appendChild(n);
var newAmount = document.getElementById('amount').value;
var a = document.createTextNode(newAmount);
newLi.appendChild(a);
var boxhold = document.getElementById('boxhold').getElementsByTagName('ul')[0];
document.body.appendChild(newLi);
};
//Adding stuff
var displayData = (function() {
var addInput = function() {
var data = userInput();
var item = newSugar();
var box = newBox();
//var box = newItem();
};
var addFood = document.getElementById('addFood');
addFood.addEventListener('click', addInput);
document.addEventListener('keypress', function(event) {
if (event.keyCode === 13 || event.which === 13) {
addInput();
}
});
})(userInput, newSugar, newBox);
Welcome to Stack Overflow #nzart 👋
It looks like you're appending the newly created list item to the document's body, which means it will be added as the last element of the page. Your CSS indicates that the styles only apply to list items inside of an unordered list, so this would explain the lack of styles.
The simple fix should be to replace document.body.appendChild(newLi); with boxhold.appendChild(newLi);. I hope this helps!