JavaScript querySelectorAll() still discovers DOM elements that have been removed - javascript

The Problem
I'm creating a treasure hunt web app that allows you to dynamically add and remove point from the hunt. I do this through the .createElement() and .remove() methods respectively.
When all points have been configured, I grab all the elements (each node is created with a custom web component) with a querySelectorAll(), iterate through them, grab all the info (title, location, clue etc.) and create an object for each point, which is then put in an array. However, if I remove a node before or after I try to save, the deleted element is not removed from the list returned by querySelectorAll(). It throws the error:
Uncaught TypeError: markers[i].shadowRoot.querySelector(...) is null
when reaching the point of any deleted points.
Method for web component removal
// Deletes point marker
deletePoint() {
const delPoint = this.shadowRoot.querySelector(".del-btn");
let pointMarker = delPoint.parentNode.parentNode.parentNode;
pointMarker.remove();
};
Add and save functions
const addPoint = document.querySelector(".add");
const savePoints = document.querySelector(".save");
var data = [];
// Defines markers in preperation for later
let markers = null
// Adds point-marker element to markers div
addPoint.addEventListener("click", () => {
const pointContainer = document.querySelector(".markers");
const node = document.createElement("point-marker");
pointContainer.appendChild(node);
});
// Grabs all point-marker elements, grabs relevant data and adds it to data array
savePoints.addEventListener("click", () => {
// clears data
data = []
markers = document.querySelectorAll("point-marker");
// Iterates through markers
for (i = 0; i < markers.length; i++) {
console.log(`i: ${i}`)
// Grabs all relevant info
let name = markers[i].shadowRoot.querySelector(".name").textContent;
let location = markers[i].shadowRoot.querySelector(".location").textContent;
let clue = markers[i].shadowRoot.querySelector("#clue").value;
// Saves all relevant info in object form
point = {
id: `${i}`,
name: `${name}`,
location: `${location} ${i}`,
clue: `${clue}`
}
// Adds point to data
data.push(point)
}
console.log(data)
});
I'm fairly certain it's an issue with the .remove() method not fully removing the element from the DOM, as it doesn't cause an issue when an element is added, but cannot find another method.
Here's the full code as a snippet if it's of any help:
// === script.js ====
// Declares template variable, containing the html template for the component
const template = document.createElement("template");
template.innerHTML = `
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.3/css/all.css" integrity="sha384-SZXxX4whJ79/gErwcOYf+zWLeJdY/qpuqC4cAa9rOGUstPomtqpuNWT9wdPEn2fk" crossorigin="anonymous">
<link rel="stylesheet" href="css/style.css">
<style>
#import url('https://fonts.googleapis.com/css2?family=Roboto:wght#400;700&display=swap');
.point-marker {
color: var(--tertiary-color);
background-color: var(--secondary-color);
padding: 2rem;
border-radius: 20px;
margin: 1rem 0;
}
.point-marker h2 {
line-height: 1rem;
}
.point-marker textarea {
width: 100%;
height: 100px;
border-radius: 20px;
resize: vertical;
padding: .5rem;
margin: 1rem 0;
}
.btn {
background-color: var(--primary-color);
border: none;
padding: .5rem 1rem;
min-width: 200px;
color: var(--tertiary-color);
border-radius: 10px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
font-size: medium;
cursor: pointer;
}
.del-btn {
background-color: var(--fail-color);
}
.btns {
display: flex;
width: 100%;
justify-content: space-evenly;
}
.coll-content {
max-height: 0;
overflow: hidden;
transition: max-height 250ms ease-in-out;
}
.collapse-icon {
font-size: large;
}
.const-content {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
</style>
<section class="point-marker">
<div class="const-content">
<h2 class="name">New Point</h2>
<i class="fas fa-minus collapse-icon"></i>
</div>
<div class="coll-content">
<p>Location: <p class="location">location</p></p>
<p>Clue:</p>
<textarea name="clue" id="clue" cols="30" rows="10"></textarea>
<div class="btns">
<button class="btn loc-btn">SET CURRENT LOCATION</button>
<button class="btn del-btn">DELETE POINT</button>
</div>
</div>
</section>
`;
// Declares class PointMarker and casts it as an HTML element
class PointMarker extends HTMLElement {
// Initialises the class every time new object is made
constructor() {
super();
// Declares shadow DOM and sets it to open
this.attachShadow({
mode: "open"
});
this.shadowRoot.appendChild(template.content.cloneNode(true));
setTimeout(() => {
const coll = this.shadowRoot.querySelector(".const-content");
coll.nextElementSibling.style.maxHeight = `${coll.nextElementSibling.scrollHeight}px`;
}, 100)
const name = this.shadowRoot.querySelector(".name")
name.contentEditable = "true";
};
// Collapses or expands the collapsable content
expandCollapse() {
const coll = this.shadowRoot.querySelector(".const-content");
let content = coll.nextElementSibling;
if (content.style.maxHeight) {
content.style.maxHeight = null;
} else {
content.style.maxHeight = `${content.scrollHeight + 30}px`;
};
};
// Deletes point marker
deletePoint() {
this.disconnectedCallback();
const delPoint = this.shadowRoot.querySelector(".del-btn");
let pointMarker = delPoint.parentNode.parentNode.parentNode;
pointMarker.remove();
pointMarker = null;
};
// Adds event listener on all elements with class of const-content or del-btn
connectedCallback() {
this.shadowRoot.querySelector(".collapse-icon").addEventListener("click", () => this.expandCollapse());
this.shadowRoot.querySelector(".del-btn").addEventListener("click", () => this.deletePoint());
console.log("connectedCallback() called");
console.log(this.isConnected)
};
// Adds event listener on all elements with class of del-btn
disconnectedCallback() {
this.shadowRoot.querySelector(".collapse-icon").removeEventListener("click", () => this.expandCollapse());
this.shadowRoot.querySelector(".del-btn").removeEventListener("click", () => this.deletePoint());
console.log("disconnectedCallback() called");
console.log(this.isConnected)
};
};
// Defines <point-marker>
window.customElements.define("point-marker", PointMarker);
const addPoint = document.querySelector(".add");
const savePoints = document.querySelector(".save");
// Defines markers in preperation for later
// Adds point-marker element to markers div
addPoint.addEventListener("click", () => {
const pointContainer = document.querySelector(".markers");
const node = document.createElement("point-marker");
pointContainer.appendChild(node);
});
// Grabs all point-marker elements, grabs relevant data and adds it to data array
savePoints.addEventListener("click", () => {
// clears data
let data = []
markers = document.querySelectorAll("point-marker");
// Iterates through markers
for (i = 0; i < markers.length; i++) {
// Grabs all relevant info
let name = markers[i].shadowRoot.querySelector(".name").textContent;
let location = markers[i].shadowRoot.querySelector(".location").textContent;
let clue = markers[i].shadowRoot.querySelector("#clue").value;
// Saves all relevant info in object form
let point = {}
point = {
id: `${i}`,
name: `${name}`,
location: `${location} ${i}`,
clue: `${clue}`
}
// Adds point to data
data.push(point)
console.log(data)
}
return data;
});
/* style.css */
#import url('https://fonts.googleapis.com/css2?family=Roboto:wght#400;700&display=swap');
:root {
--primary-color: #FA4D05;
--secondary-color: #333;
--tertiary-color: #fff;
--success-color: #97FD87;
--fail-color: #FF5555;
--bg-color: #E5E5E5;
--font-color: #808080;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
}
html {
scroll-behavior: smooth;
}
body {
min-height: 100vh;
line-height: 2;
color: var(--primary-color);
}
h1 {
font-size: 36px;
}
h2 {
font-size: 24px;
}
nav {
display: flex;
background-color: var(--secondary-color);
justify-content: space-between;
align-items: center;
height: 65px;
padding-left: 5rem;
/* color: var(--primary-color); */
}
nav ul {
list-style: none;
display: flex;
justify-content: space-evenly;
width: 50%;
}
main {
display: flex;
flex-direction: column;
padding: 2rem;
}
main h1 {
margin-bottom: 1rem;
}
.btn {
background-color: var(--primary-color);
border: none;
padding: .5rem 1rem;
min-width: 200px;
color: var(--tertiary-color);
border-radius: 10px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
font-size: medium;
cursor: pointer;
}
.add-point {
background-color: var(--bg-color);
color: var(--font-color);
margin: 1rem 0;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.save {
background-color: var(--success-color);
}
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.3/css/all.css" integrity="sha384-SZXxX4whJ79/gErwcOYf+zWLeJdY/qpuqC4cAa9rOGUstPomtqpuNWT9wdPEn2fk" crossorigin="anonymous">
<title>Create A Hunt</title>
</head>
<body>
<header>
<nav>
<h2>HOME</h2>
<ul>
<li>
<h2>HUNT</h2>
</li>
<li>
<h2>CREATE</h2>
</li>
</ul>
</nav>
</header>
<main>
<h1>CREATE A HUNT</h1>
<div class="markers">
</div>
<button class="btn add-point add">
<h2>Add Point +</h2>
</button>
<button class="btn add-point save">
<h2>Save Points</h2>
</button>
</main>
<script src="script.js"></script>
<script src="/components/pointMarker.js"></script>
</body>
</html>
TL;DR
Elements removed with the .remove() method are still picked up by the .querySelectorAll() method, presumably because it does not remove it from the DOM fully.

// Deletes point marker
deletePoint() {
this.disconnectedCallback();
const delPoint = this.shadowRoot.querySelector(".del-btn");
let pointMarker = delPoint.parentNode.parentNode.parentNode;
pointMarker.remove();
pointMarker = null;
};
This does not remove the point marker. It removes the contents of the point marker but the point marker is still there.
// Deletes point marker
deletePoint() {
this.disconnectedCallback();
this.remove();
};
This removes the actual element from the page, and your code then works just fine.

Related

How to clear the innerHTML content of an HTMLCollection?

I am building a Tic Tac Toe game. My problem is being unable to clear the innerHTML of multiple divs (div class .cell) when clicking the resetButton. The divs I am trying to clear are the children of gameDisplay.
When I tried to locate the cell elements in my chrome dev tool, I get an HTML Collection:
ScreenShot Here
I am having difficulty accessing the innerHTML of these elements. I currently have my resetGame() function returning gameDisplay.innerHTML=""(line 79) however, that removes all of the content of the parent element and removes my grid(board) completely. I know that is not specific enough. As a result, I tried returning gameDisplay.children.innerHTML =""; instead but nothing happens in the DOM or in my chrome dev tools console.
How can I access the innerHTML of these divs and remove the HTML inside of them using the resetGame function?
JS
const playerFactory = () => {
const playTurn = (event, currentPlayer) => {
const id = boardObject.cells.indexOf(event.target);
boardObject.boardDisplay[id] = currentPlayer;
boardObject.render();
};
return {
playTurn
};
};
// Gameboard object
const boardObject = (() => {
let boardDisplay = ['', '', '', '', '', '', '', '', ''];
const cells = Array.from(document.querySelectorAll(".cell"));
// displays the content of the boardArray
const render = () => {
boardDisplay.forEach((cells, idx) => {
cells[idx].innerHTML = boardDisplay[idx];
});
};
return {
boardDisplay,
render,
cells
};
})();
// Create Array from DOM
const boardArray = [...document.querySelectorAll(".cell")].map(cells=>cells.innerHTML);
console.log(boardArray);
// Display controller, shows which player is which
const displayController = (() => {
const playerOne = 'X';
const playerTwo = 'O';
const gameBoard = document.querySelector(".game-board");
let currentPlayer = playerOne;
const switchPlayer = () => {
currentPlayer = currentPlayer === playerOne ? playerTwo : playerOne;
}
gameBoard.addEventListener("click", (event) => {
if (event.target.classList.contains("cell")) {
if (event.target.textContent === "") {
event.target.textContent = currentPlayer;
const indexOfClickedCell = Array.prototype.indexOf.call(document.querySelectorAll('.cell'), event.target);
boardArray[indexOfClickedCell] = currentPlayer;
switchPlayer();
}
}
});
})();
const gameDisplay = document.getElementById("game-board");
// Define reset button function first
function resetGame(){
// Define result, reset the board
//if the board array index is equal to a character
if(boardArray.includes('X','O')){
{ return gameDisplay.innerHTML = "";
}
}
else
{;}
//and if the cells textContent is equal to a character
//return the result, which is the reset board function
}
const resetButton = document.querySelector(".restart");
resetButton.addEventListener("click", (event) => {
resetGame(event);
});
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://use.typekit.net/xtq6hun.css">
<link rel="stylesheet" href="styles.css">
<title>TIC-TAC-TOE</title>
</head>
<body>
<main>
<div class="top-text">
<h1>TIC TAC TOE</h1>
<div id ="game-text">
</div>
</div>
<div class="game-board" id="game-board">
<div class="cell" id="cell-1"></div>
<div class="cell" id="cell-2"></div>
<div class="cell" id="cell-3"></div>
<div class="cell" id="cell-4"></div>
<div class="cell" id="cell-5"></div>
<div class="cell" id="cell-6"></div>
<div class="cell" id="cell-7"></div>
<div class="cell" id="cell-8"></div>
<div class="cell" id="cell-9"></div>
</div>
<div class="bottom-box">
<button class="restart">RESTART GAME</button>
</div>
</main>
<script src="script.js" defer></script>
</body>
</html>
CSS
/*CSS RESET*/
* {
margin:0;
padding:0;
}
body {
background-color: #faf9fa;
}
main {
display: flex;
align-content: center;
flex-direction: column;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
}
h1 {
font-family: blackest-text, sans-serif;
font-weight: 700;
font-style: normal;
font-size: 60px;
color: #38393a;
}
h2 {
font-family: elza, sans-serif;
font-weight: 500;
font-style: normal;
font-size: 20px;
color: #38393a;
}
.top-text {
border: none;
margin: 0.5em;
display: flex;
flex-direction: column;
align-items: center;
}
.game-board {
display: grid;
grid-template-columns: repeat(3, 1fr);
border: 1px solid #38393a;
margin: 0px auto;
background-color: #faf9fa;
margin-bottom: 2em;
}
.cell:hover {
background-color: rgb(221, 253, 228);
}
.cell {
/*STYLING X'S AND O'S*/
font-family: blackest-text, sans-serif;
font-weight: 700;
font-style: normal;
font-size: 100px;
color: #38393a;
/*STYLING INDIVUAL CELLS*/
min-height: 170px;
min-width: 170px;
border: 1px solid #38393a;
display: flex;
align-content: center;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
}
img {
width: 90px;
color:#38393a;
}
.message-display {
border: none;
margin-bottom: 1em;
}
.restart{
font-family: elza, sans-serif;
font-weight: 500;
font-style: normal;
height: 3.5em;
width: 12em;
border-radius: 30px;
border: 1px solid #38393a;
background-color: #faf9fa;
padding: 1px;
}
.restart:hover{
background-color: rgb(221, 253, 228);
}
span {
font-family: blackest-text, sans-serif;
font-weight: 700;
font-style: normal;
font-size: 100px;
color: #38393a;
}
p {
font-family: blackest-text, sans-serif;
font-weight: 700;
font-style: normal;
font-size: 30px;
color: #38393a;
}
In your resetGame function you should loop over all the cells and remove their content instead of deleting the whole game-board content.
for(let i =0;i < gameDisplay.children.length;i++) {
gameDisplay.children[i].innerHTML = '';
}

How do I use the input from my box to my function?

So I created a 16 x 16 grid where I can etch a sketch on that grid. It's working well. Right now, I have to call the function createGrid(number) everytime I want to change the size of the grid. I have created a text input boxes as you can see on my code. So instead of having to write it again everytime and refresh the page, I want to be able to use the input from this box to change the size of the grid.
One of the ways that I've tried is by creating a new variable such as:
let number = inputFromBox.value;
and then write createGrid(number) . But it doesn't work. Is there any way how to make this work ? by using the input from the box ? Please help. Thank you !
let container = document.querySelector('.container');
let rows = document.getElementsByClassName('gridRow');
let duplicateOfInput = document.getElementById('duplicateOfInput');
let button = document.getElementById('submit');
let inputFromBox = document.getElementsByClassName('size-box');
button.addEventListener('click', createGrid);
createGrid(16);
draw();
// createGrid(anyNumber);
// draw();
// duplicateOfInput.textContent = `${input}`;
// const rainbow = document.getElementsByClassName('rainbow');
let reset = document.getElementById('clear-button');
reset.addEventListener('click', clearGrid);
function createGrid(number) {
makeRow(number);
makeColumn(number);
draw();
}
function makeRow(numberOfRow) {
container.innerHTML = "";
for (let i = 0; i <numberOfRow; i++) {
let row = document.createElement('div');
container.appendChild(row);
row.classList.add('gridRow');
}
}
function makeColumn(numberOfColumn) {
for ( let i = 0; i < rows.length; i++) {
for ( let j = 0; j < numberOfColumn; j++) {
let column = document.createElement('div');
rows[j].appendChild(column);
column.classList.add('gridColumn');
}
}
}
//adds event listener to all divs with class "column"
//added in global scope to allow drawing on page load
//this refers to the element triggering the mouseover event listener
function draw() {
let columns = document.getElementsByClassName("gridColumn");
for (let i = 0; i < columns.length; i++) {
columns[i].addEventListener("mouseover", changeColor);
}
function changeColor() {
let blue = document.getElementById('blue');
let eraser = document.getElementById('eraser');
let black = document.getElementById('black');
let rainbow = document.getElementById('rainbow');
if (blue.checked) {
this.style.backgroundColor = 'blue';
} else if (eraser.checked) {
this.style.backgroundColor = 'beige';
} else if (black.checked) {
this.style.backgroundColor = 'black';
} else if (rainbow.checked) {
let randomColor = Math.floor(Math.random()*16777215).toString(16);
this.style.backgroundColor = '#' + randomColor;
}
}
}
//eraser function loops through all column divs and sets background to "" in DOM
function clearGrid() {
let columns = document.getElementsByClassName("gridColumn");
for (let i = 0; i < columns.length; i++) {
columns[i].style.backgroundColor = '';
}
}
#import url('https://fonts.googleapis.com/css2?family=Asap:wght#400;600;700&display=swap');
body {
display: flex;
height: 100%;
width: 100%;
flex-direction: column;
background-color: beige;
font-family: Asap, sans-serif;
margin: 0;
padding: 0;
justify-content: center;
align-content: center;
align-items: center;
background-repeat: no-repeat;
}
.header {
display: flex;
flex: 1;
justify-content: center;
}
#header-title {
font-family: Asap, sans-serif;
font-size: 18px;
}
#setGridSize {
display: inline-flex;
justify-content: center;
align-items: center;
flex: 1;
gap: 12px;
}
#guide {
text-align: center;
margin: 1px;
font-family: Asap, sans-serif;
color: red;
font-size: 13px;;
}
.canvas {
display: flex;
justify-content: center;
align-items: center;
}
.container {
display: flex;
flex-direction: column;
border: 1px solid green;
width: 550px;
height: 550px;
}
/* .gridColumn {
display: inline-flex;
border: 1px solid beige;
margin: -1px 0;
width: 30px;
height: 30px;
} */
.gridColumn {
flex: 1;
border: 1px solid beige;
}
.gridRow {
display: flex;
flex: 1;
}
.default {
background: beige;
}
#button-container {
margin: 3px;
}
#clear-button {
margin: 2px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DOM Manipulation and Events</title>
<script src="javascript.js" defer></script>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1 class="header"> Let's sketch ! </h1>
<div id="setGridSize">
<p id="header-title"> Grid Size :</p>
<input type="text" placeholder="Size of Board" class="size-box">
<span id = "duplicateOfInput"></span>
<button id="submit" > Submit </button>
</div>
<p id="guide"> Enter a number between 2 to 99 </p>
<div class="canvas">
<div class="container"></div>
</div>
<div class="buttons">
<form id="button-container">
<input type="radio" id="blue" name="pen" value="blue-pen"><label for = "blue-pen"> Blue </label>
<input type="radio" id="eraser" name="pen" value="eraser"><label for = "eraser" > Eraser </label>
<input type="radio" id="black" name="pen" value="black-pen"> <label for = "black" > Black </label>
<input type="radio"id="rainbow" name="pen" value="black-pen"> <label for = "rainbow" > Rainbow </label>
</form>
</div>
<button id = "clear-button" > Clear </button>
</body>
</html>
You can use let inputFromBox = document.getElementById('size-box'); and with let number = inputFromBox.value; you can get the value within the click function.
CodePen
You're using document.getElementsByClassName, which is most likely returning an array. Try using inputFromBox[0].value or switching to finding it by an ID.
You may find it easier to use CSS Grid. It allows you to minimise the amount of code you write because you don't have to create rows and columns separately.
Additionally if you set up some CSS variables you can hook the value from the input directly into the stylesheet to update the dimensions of the grid.
// Cache the elements
const grid = document.querySelector('.grid');
const submit = document.querySelector('.submit');
const input = document.querySelector('.size');
// When the button is clicked call `createGrid`
submit.addEventListener('click', createGrid);
function createGrid() {
// Get the value from the input
const { value } = input;
// Array to hold the box strings
const boxes = [];
// Loop to create a grid of boxes - for each
// box push it into the array
for (let i = 1; i <= value * value; i++) {
boxes.push(`<div class="box">${i}</div>`);
}
// `join` the array and update the innerHTML
// of the grid element
grid.innerHTML = boxes.join('');
// Pass the value directly into the spreadsheet.
// This changes the default `--grid-dimension` to the new value
document.documentElement.style.setProperty('--grid-dimension', value);
}
:root { --box-width: 30px; --grid-dimension: 13; }
.grid { margin-top: 1em; display: grid; grid-template-columns: repeat(var(--grid-dimension), var(--box-width)); gap: 0.2em; }
.box { display: flex; justify-content: center; align-items: center; height: var(--box-width); width: var(--box-width); background-color: lightgreen; }
.box:hover { background-color: lightblue; cursor: pointer; }
<input type="text" placeholder="Size of grid" class="size">
<button type="button" class="submit">Create grid</button>
<div class="grid"></div>

How to add event handlers on elements created dynamically which are not present at the start [duplicate]

This question already has answers here:
Event binding on dynamically created elements?
(23 answers)
Closed 9 months ago.
I am very new to javascript and currently am making a to-do list like this.
I am trying to make the list item turn green when clicked using javascript. This is my current code:
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TODO</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght#300;400&family=PT+Sans&family=Roboto:wght#300;400;500;700&family=Rubik:wght#700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap#4.1.3/dist/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<script src="https://kit.fontawesome.com/7fa743e7fb.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="app.css">
</head>
<body>
<h1>WORK TO-DOS</h1>
<p id="desc">Enter text into the input field to add items to your list.</p>
<p id="compl-desc">Click the item to mark it as complete.</p>
<p id="rem-desc">Click the "X" to remove the item from your list.</p>
<section id="input-block">
<input type="text" name="todo" id="todo" placeholder="New Item">
<button id="submit"><i class="fa-solid fa-file-pen"></i></button>
</section>
<ul class="todo-area"></ul>
<script src="app.js"></script>
<!-- tst div -->
<!-- <div class='test-div'>
<span>lor</span>
<button id="delete-li">
<i class="fa-solid fa-xmark"></i>
</button>
</div> -->
</body>
</html>
CSS:
body {
height: 100vh;
background-color: #04a1bf;
display:flex;
flex-direction: column;
align-items: center;
/* justify-content: center; */
}
h1 {
font-size: 3.0rem;
color: white;
font-family: 'Rubik', sans-serif;
}
p {
font-family: 'Roboto', sans-serif;
font-weight: 500;
}
/* changing colors */
#desc {
color: yellow;
}
#compl-desc {
color:greenyellow
}
#rem-desc {
color: #025f70;
}
section {
width: 70%;
height: 60px;
}
#todo {
height: 55%;
width: 90%;
border-radius: 5px;
border: none;
/* margin-right: 10px; */
}
#submit {
border: none;
height: 55%;
width: 7%;
font-size: 0.8rem;
border-radius: 0.5em;
background-color: #025f70;
color:aliceblue;
transition-property: all;
transition-duration: 0.5s;
/* transition-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1); */
}
#submit:hover {
background-color: #02798f;
color: bisque;
cursor: pointer;
height: 55%;
/* border: 1px solid black; */
}
.todo-area {
list-style-type: none;
padding:0%;
width: 60%;
}
.todo-item {
/* width: 60%; */
background-color: #4eb9cd;
padding: 8px;
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 10px;
border-color: #025f70;
border-width: 3px;
border-style: solid;
transition: background-color 0.5s;
margin: 10px 0px;
}
.todo-item:hover {
background-color: #76cfe0;
}
.todo-item-complete {
background-color: #51df70;
color: #0f8d2a;
}
#delete-li {
border: none;
background-color: #4eb9cd;
margin-right:2%;
transition: background-color 0.5s;
}
.todo-item:hover #delete-li {
background-color: #76cfe0;
}
Javascript:
const todoUl = document.querySelector('.todo-area');
const input = document.querySelector('#todo');
const addBtn = document.querySelector('#submit');
console.log('input', input['value'])
function createLi(inputText) {
// this function creates the li with the delete button
// creating the Li first
const li = document.createElement('li');
// add class to li
li.classList.add('todo-item');
// making the button
const delBtn = document.createElement('button');
delBtn.setAttribute('id', 'delete-li');
// icon
const btnIcon = document.createElement('i');
btnIcon.classList.add('fa-solid');
btnIcon.classList.add('fa-xmark');
// adding that to the delete button
delBtn.appendChild(btnIcon)
// adding text to the element from the input
// creating a span
const liSpan = document.createElement('span');
liSpan.innerText = inputText;
li.appendChild(liSpan);
li.appendChild(delBtn);
return li
}
addBtn.addEventListener('click', () => {
// getting value in text box
let todoText = input.value;
console.log('button clicked');
console.log(todoText);
// adding to the ul element
if(todoText !== "") {
let newLi = createLi(todoText);
// adding class to the li
// newLi.classList.add('todo-item');
todoUl.appendChild(newLi);
}
// resetting the text box blank again
input.value = '';
})
// getting all the list items
const listItems = document.querySelectorAll('.todo-item');
// turning them green once they are clicked
for(let li of listItems) {
// adding an onclick event on them
li.addEventListener('click', () => {
// adding the active class
console.log('LI CMD');
li.classList.toggle('todo-item-complete');
})
}
Which clearly is not working. Although I think I have an idea why is it not working. I need to know how to set event handlers on elements that are dynamically generated as a result of some action in the page.
you should add eventListener while creating the li
like
function createLi(inputText) {
// this function creates the li with the delete button
// creating the Li first
const li = document.createElement('li');
// add class to li
li.classList.add('todo-item');
// making the button
const delBtn = document.createElement('button');
delBtn.setAttribute('id', 'delete-li');
// icon
const btnIcon = document.createElement('i');
btnIcon.classList.add('fa-solid');
btnIcon.classList.add('fa-xmark');
// adding that to the delete button
delBtn.appendChild(btnIcon)
// adding text to the element from the input
// creating a span
const liSpan = document.createElement('span');
liSpan.innerText = inputText;
li.appendChild(liSpan);
li.appendChild(delBtn);
li.addEventListener('click', () => {
// adding the active class
console.log('LI CMD');
li.classList.toggle('todo-item-complete');
})
return li
}

Stop underline from getting bigger

I'm creating a currency converter app using html,css and javascript, and when text is entered into the <input> on the left, the converted value will appear in the input element on the right: <p id = "converted">.
I want to keep the underline(border-bottom) the same length on the <p id = "converted">. Currently, when you enter text into the input element on the left, the one on the right increases in size and makes the underline larger. I want the underline to stay the same as when there is no text in the element.
I am currently styling the <p id = "converted"> element like so:
padding-right: 40%;
border-bottom: 0.5vh solid white;
I do not think the API I am using will work correctly with the stack overflow snippets, but I will include a link to a codepen: https://codepen.io/oliknight/pen/XLvQow
let currlet currencyArr = [];
let ratesArr = [];
window.addEventListener("load", () => {
const api = "https://api.exchangeratesapi.io/latest?base=GBP";
fetch(api)
.then(response => {
return response.json();
})
.then(data => {
for (currency in data.rates) {
currencyArr.push(currency);
ratesArr.push(data.rates[currency]);
// create 'option' element here
var optionLeft = document.createElement("option");
var optionRight = document.createElement("option");
optionLeft.textContent = currency;
optionRight.textContent = currency;
document.querySelector("#left-select").appendChild(optionLeft);
document.querySelector("#right-select").appendChild(optionRight);
}
document.querySelector("#input").addEventListener("keyup", convert);
function convert() {
const input = document.querySelector("#input");
let leftSelectValue = document.querySelector("#left-select").value;
let convertedNumber = document.querySelector("#converted");
for (let i = 0; i < currencyArr.length; i++) {
if (leftSelectValue === currencyArr[i]) {
convertedNumber.textContent = ratesArr[i].toFixed(4) * input.value;
}
}
}
});
});
encyArr = [];
let ratesArr = [];
window.addEventListener("load", () => {
const api = "https://api.exchangeratesapi.io/latest?base=GBP";
fetch(api)
.then(response => {
return response.json();
})
.then(data => {
for (currency in data.rates) {
currencyArr.push(currency);
ratesArr.push(data.rates[currency]);
// create 'option' element here
var optionLeft = document.createElement("option");
var optionRight = document.createElement("option");
optionLeft.textContent = currency;
optionRight.textContent = currency;
document.querySelector("#left-select").appendChild(optionLeft);
document.querySelector("#right-select").appendChild(optionRight);
}
document.querySelector("#input").addEventListener("keyup", convert);
function convert() {
const input = document.querySelector("#input");
let leftSelectValue = document.querySelector("#left-select").value;
let convertedNumber = document.querySelector("#converted");
for (let i = 0; i < currencyArr.length; i++) {
if (leftSelectValue === currencyArr[i]) {
convertedNumber.textContent = ratesArr[i].toFixed(4) * input.value;
}
}
}
});
});
html {
font-family: "Lato", sans-serif;
font-weight: thin;
background-image: linear-gradient(to right top, #90d0ff, #008ef7);
color: white;
height: 100vh;
overflow: hidden;
}
h1 {
text-align: center;
font-size: 5em;
position: absolute;
left: 0;
right: 0;
margin: auto;
}
.container {
width: 50%;
display: flex;
justify-content: center;
align-items: center;
}
.container p {
font-size: 8em;
display: inline-block;
padding-right: 40%;
border-bottom: 0.5vh solid white;
max-width: 50%;
}
.parent {
display: flex;
justify-content: center;
height: 100vh;
align-items: center;
}
.container select {
background: transparent;
color: white;
padding: 20px;
width: 80px;
height: 60px;
border: none;
font-size: 20px;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.5);
-webkit-appearance: button;
outline: none;
margin-left: 10%;
}
.original {
background: transparent;
border: none;
border-bottom: 0.5vh solid white;
font-size: 8em;
max-width: 50%;
outline: none;
font-family: "Lato", sans-serif;
font-weight: thin;
color: white;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Currency Converter</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="main.css" />
<link href="https://fonts.googleapis.com/css?family=Lato:300&display=swap" rel="stylesheet" />
</head>
<body>
<h1>Currency Converter</h1>
<div class="parent">
<div class="container">
<input type="text" class="original" id="input" />
<select id="left-select"> </select>
</div>
<div class="container" id="ctn">
<p id="converted">0</p>
<select id="right-select"> </select>
</div>
</div>
<script src="main.js"></script>
</body>
</html>
Thanks!
If you replace max-width with width in your css, the percentage (e.g. 50%) width will stay the same, regardless of how long the converted number is. However this poses a problem regarding your overflow.
You could remove the padding, but you could still run into issues whereby only half a digit is showing at the end, depending on how big the device is:
So you may want to either reduce the font size or experiment with width percentages (maybe 49 or 51 .. ) , or both ..
If you knew exactly how many decimal digits there would be in the returned converted number, this would help determine an adequate size font.
Good luck, and hope this helps

Stop named function from executing itself

I am trying to build a simple webpage, which simply checks the value of the strings in two input fields, such that when the Test button is clicked, a previously hidden div will show the boolean value to be returned (If isomorphic return true, else, return false).
This is my code:
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Isomorphics App</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" media="screen" href="styles.css" />
</head>
<body>
<div class="container">
<div class="content">
<div class="header">
<p class="title">Isomorphics</p>
<span> </span>
<p class="description">Find out if any two strings are isomorphic.</p>
<span> </span>
<p>Enter any two words (strings) in the fields below:</p>
</div>
<div class="input-container">
<input class="input" id="s" placeholder="First string" />
<input class="input" id="t" placeholder="Second string" />
<button class="input-button" id="submit-button">Test</button>
</div>
<div class="isomorphic-state-container" id="isomorphic-state-container">
<div class="isomorphic-state-holder" id="isomorphic-state-holder">
<p class="isomorphic-state" id="isomorphic-state">ili</p>
</div>
</div>
</div>
</div>
<script src="main.js"></script>
</body>
</html>
main.js
let s = document.getElementById('s').innerText;
let t = document.getElementById('t').innerText;
console.log(s);
document.getElementById('submit-button').onclick = isomorphic(s, t);
console.log(isomorphic(s, t));
function isomorphic(str1, str2) {
if (str1.length !== str2.length) {
alert('Please enter two strings of equal length.');
}
let map = {};
for (let i = 0; i < str1.length; i++){
let a = str1[i];
let b = str2[i];
if (typeof map[a] === 'undefined') {
map[a] = b;
} else if (map[a] !== b) {
// alert(false);
document.getElementById('isomorphic-state-container').style.display = 'block';
document.getElementById('isomorphic-state-holder').style.backgroundColor = 'red';
document.getElementById('isomorphic-state').innerText = 'False'
}
for (var key in map) {
if (key !== a && b === map[key]) {
// alert(false);
document.getElementById('isomorphic-state-container').style.display = 'block';
document.getElementById('isomorphic-state-holder').style.backgroundColor = '#D64BFB';
document.getElementById('isomorphic-state').innerText = 'False'
}
}
}
document.getElementById('isomorphic-state-container').style.display = 'block';
document.getElementById('isomorphic-state-holder').style.backgroundColor = 'green';
document.getElementById('isomorphic-state').innerText = 'True'
}
styles.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Roboto Mono;
font-size: 16px;
}
*:focus {
outline: none;
}
.header {
color: #FFFFFF;
text-align: center;
}
.header .title {
font-size: 36px;
}
.header .description {
font-size: 16px;
}
.container {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
width: 100vw;
background-color: #000000;
}
.container .content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
height: 65%;
width: 100%;
}
.container .content .input-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
width: 50%;
height: 150px;
}
.container .content .input-container .input {
width: 100%;
height: 40px;
font-size: 16px;
padding-left: 10px;
}
.container .content .input-container .input-button {
width: 100%;
height: 40px;
background-color: rgba(255, 255, 255, 0.1);
color: #FFFFFF;
font-size: 16px;
}
.container .content .input-container .input-button:hover {
cursor: pointer;
}
.isomorphic-state-container {
display: none;
width: 50%;
height: 40px;
}
.isomorphic-state-holder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.isomorphic-state {
color: #FFFFFF;
font-size: 16px;
}
After running this code, the javascript defaults the value to true, and doesn't work even when I click the button. Screenshot:
What should I do to make the code run only when the button is clicked? Or, is the named function causing the problem?
This line:
document.getElementById('submit-button').onclick = isomorphic(s, t);
calls your function and then assigns its return value to onclick, exactly the way x = foo() calls foo and assigns its result to x. To set a click event handler that way, you assign a function (not its result) to the onclick property, for instance:
document.getElementById('submit-button').onclick = function() {
isomorphic(s, t);
};
(You'll also want to remove the console.log(isomorphic(s, t)); line, since it also calls the function.)
If you want to get s and t as of when the button is clicked, instead of when the script first runs, move those lines into the click handler as well:
document.getElementById('submit-button').onclick = function() {
let s = document.getElementById('s').innerText;
let t = document.getElementById('t').innerText;
isomorphic(s, t);
};
Better yet, use modern event handling via addEventListener:
document.getElementById('submit-button').addEventListener("click", function() {
// ...handler code...
});
If you need to support obsolete browsers like IE8, this answer has a workaround to their lack of addEventListener support.
There are several other issues with your code, though. For instance, to get the value of an input element, you use its value property, not its innerText property (it doesn't have any inner text, because it's a void element). You're also not returning after your alert about unequal string lengths, which you probably want to do since otherwise you go ahead and run the body of the function even though you did the alert...

Categories