Here's the code i've written, where when i write a word into e search field, it appends it in the element "word" by displaying it letter by letter. But, the problem is, that i don't know how to write the code that when i write another word in the search field, it deletes the word that appear to element "Word", then writes the new one i've written.
let text = document.getElementById("txt");
let elem = document.getElementsByClassName("target")[0];
let word = elem.querySelector(".word");
let btn = document.getElementsByClassName("btn")[0];
let error = document.querySelector('#error');
i = 0;
word.style.color = "#ffe100";
btn.addEventListener("click", function init() {
if (text.value == "") {
error.style.opacity = '1.0';
} else {
error.style.opacity = '0.0';
let save = word.textContent += text.value.charAt(i);
i++;
}
if (i < text.value.length) {
window.setTimeout(init, 100);
}
});
I've try many of alternatives, but there's no result.
I will iteratively change and/or improve your code in this answer, and will try to comment on each change in the code.
Refactor
First off, I'll have an easier time explaining the different approaches after refactoring your code:
// Prefer `const` over `let` when applicable
const input = document.querySelector("input"); // Previously: text
const output = document.querySelector("output"); // Previously: word
const button = document.querySelector("button"); // Previously: btn
const error = document.getElementById("error");
// You forgot `let`. Always declare your variables!
let i = 0;
button.addEventListener("click", () => {
if (input.value == "") {
error.style.opacity = '1.0';
} else {
error.style.opacity = '0.0';
reveal();
}
});
// Moved code from listener to here
function reveal() {
// Removed `save`; was unused
output.textContent += input.value[i];
++i;
// Moved here because it only runs if `input.value !== ""` anyways
if (i < input.value.length) {
setTimeout(reveal, 100);
}
}
/* Prefer to use CSS for static styles! */
.word {
color: #ffe100;
}
<!-- I assume your HTML to have looked similar to this: -->
<input><button>Submit</button>
<div>
<output></output>
</div>
<p id="error">Please submit (actual) text.</p>
Now let's take a look at your refactored code from above:
There is no resetting: Revealing can only continue (text can only be added).
The value of input is referenced directly: When its value changes...
Then revealing may stop prematurely.
Then the further revealed text may not represent the entered text upon button-press.
Allow reusability
The issue of point 1 can be solved by (re-)setting i and output.textContent in the listener. To solve point 2, we need to use some buffer for the text:
const input = document.querySelector("input");
const output = document.querySelector("output");
const button = document.querySelector("button");
const error = document.getElementById("error");
let i = 0;
let text = ""; // The buffer
button.addEventListener("click", () => {
if (input.value == "") {
error.style.opacity = '1.0';
} else {
error.style.opacity = '0.0';
// (Re-)setting
i = 0;
text = input.value;
output.textContent = "";
reveal();
}
});
function reveal() {
output.textContent += text[i];
++i;
if (i < text.length) {
setTimeout(reveal, 100);
}
}
.word {
color: #ffe100;
}
<input><button>Submit</button>
<div>
<output></output>
</div>
<p id="error">Please submit (actual) text.</p>
With these two small changes, your code now successfully deletes the revealed text in place for new text!
Adding states
But the deletion doesn't happen letter-by-letter. That would require some way to keep track of whether we are deleting or revealing.
Let's use a state-machine that –upon prompting– deletes the already revealed text (if any) and then reveals the new text:
const input = document.querySelector("input");
const output = document.querySelector("output");
const button = document.querySelector("button");
const error = document.getElementById("error");
let i = 0;
let text = "";
let state = "nothing"; // The state
button.addEventListener("click", () => {
if (input.value == "") {
error.style.opacity = '1.0';
} else {
error.style.opacity = '0.0';
// Remember previous state (for below)
const previousState = state;
// Issue deletion and update text-to-reveal
state = "delete";
text = input.value;
if (previousState === "nothing") {
// Start state-machine
nextCharacter();
}
}
});
// Rename function
function nextCharacter() {
if (state === "nothing") return;
if (state === "delete") {
output.textContent = output.textContent.slice(0, i);
// Finished deleting?
if (i === 0) {
const requiresRevealing = i < text.length;
state = requiresRevealing ? "reveal" : "nothing";
} else {
--i;
}
} else if (state === "reveal") {
output.textContent += text[i];
++i
// Finished revealing?
if (i === text.length) {
state = "nothing";
}
}
// Requires continuing?
if (state !== "nothing") {
setTimeout(nextCharacter, 100);
}
}
.word {
color: #ffe100;
}
<input><button>Submit</button>
<div>
<output></output>
</div>
<p id="error">Please submit (actual) text.</p>
Code quality refactoring
The code now works, but(!) the logic is scattered everywhere, and you need to know what variables to update for the revealing to work correctly. Instead, we could make use of classes:
const input = document.querySelector("input");
const output = document.querySelector("output");
const button = document.querySelector("button");
const error = document.getElementById("error");
// Move related variables and functions into class
class Revealer {
output;
index = 0;
text = "";
state = "nothing";
constructor(output) {
this.output = output;
}
// Moved from listener
reveal(text) {
const previousState = this.state;
this.state = "delete";
this.text = text;
if (previousState === "nothing") {
this.next();
}
}
// Previously nextCharacter()
next() {
if (this.state === "nothing") return;
if (this.state === "delete") {
this.deleteCharacter();
} else if (this.state === "reveal") {
this.revealCharacter();
}
if (this.state !== "nothing") {
setTimeout(() => this.next(), 100);
}
}
// Use more specific functions for readability
deleteCharacter() {
this.output.textContent = this.output.textContent.slice(0, this.index);
if (this.index === 0) {
const requiresRevealing = this.index < this.text.length;
this.state = requiresRevealing ? "reveal" : "nothing";
} else {
--this.index;
}
}
revealCharacter() {
this.output.textContent += this.text[this.index];
++this.index;
if (this.index === this.text.length) {
this.state = "nothing";
}
}
}
const revealer = new Revealer(output);
button.addEventListener("click", () => {
if (input.value == "") {
error.style.opacity = '1.0';
} else {
error.style.opacity = '0.0';
// Use simple call
revealer.reveal(input.value);
}
});
.word {
color: #ffe100;
}
<input><button>Submit</button>
<div>
<output></output>
</div>
<p id="error">Please submit (actual) text.</p>
Ideas for practicing
While the above code works, there are still ways to improve it or implement it differently:
Only delete until the remaining text is the same as the leading substring of the text-to-add.
You can use promises (with async/await) instead of using setTimeout() directly.
You can implement the revealing as functionality of a custom element.
...
Try implementing (one of) these as practice!
I'm having two timing issues here, both involving the process in this game once the winning move has been made: https://codepen.io/acchang/pen/XWePpWB
Ideally, I should (1) pick the winning space (2) see the winning space filled (3) have the alert proclaim the winner.
What I see and do not like is:
*checkForWinners() runs
winDeclared() runs and the alert "winner" pop up first
Then after the alert is cleared, drawboard() runs, adding the winning piece to the gameboard.
This does not happen as badly in Firefox. The piece is added at the same time the alert pops up.
Then, in winDeclared(), I also change the display in the top right to also indicate the winner. But swapTurns() seems to execute before winDeclared().
Is that because winDeclared() is two functions deep into checkForWinners()? Is there a way to delay it?
Thanks!
let gameboard = [
[1,2,3,4,5,6,7],
[8,9,10,11,12,13,14],
[15,16,17,18,19,20,21],
[22,23,24,25,26,27,28],
[29,30,31,32,33,34,35],
[36,37,38,39,40,41,42]
];
let playerOne
let playerTwo
let indexPick
let availableSpots
let gameType
let playerOneTurn = true
document.getElementsByName("announcements")[0].innerHTML = "Current Player: " + whosPlaying() + " "
let itsAOnePlayerGame = true
let isThereAWinner = false
let mainDiv = document.createElement("div");
mainDiv.setAttribute('class', 'mainDiv')
document.body.append(mainDiv);
let selectorHolder = document.createElement("div")
selectorHolder.setAttribute('class', 'selectorHolder')
selectorHolder.setAttribute('id', 'selectorHolder')
mainDiv.append(selectorHolder)
let selectorTable = document.createElement("table")
selectorTable.setAttribute('class', 'selectorTable')
selectorTable.setAttribute('id', 'selectorTable')
selectorHolder.append(selectorTable)
function drawSelector() {
let selectorRow = document.createElement("tr")
selectorRow.setAttribute('class', 'selectorRow')
selectorTable.append(selectorRow)
for (i=0; i<7; i++){
let selectorCell = document.createElement("td")
selectorCell.setAttribute('class', 'selectorCell')
let innerSelectorCell = document.createElement("div")
innerSelectorCell.setAttribute('class', 'innerSelectorCell')
innerSelectorCell.setAttribute('id', [i])
selectorCell.append(innerSelectorCell)
innerSelectorCell.addEventListener("mouseover", function(event) {
if (playerOneTurn == true) {
innerSelectorCell.classList.add('yellowBG')}
else {innerSelectorCell.classList.add('redBG')
}
})
innerSelectorCell.addEventListener("mouseout", function(event) {
if (playerOneTurn == true) {
innerSelectorCell.classList.remove('yellowBG')}
else {innerSelectorCell.classList.remove('redBG')
}
})
innerSelectorCell.onclick = function(){
if (isThereAWinner == true){return}
else {
indexPick = parseInt(this.id)
console.log(indexPick)
claimSpot()
}
}
selectorRow.append(selectorCell)
}
};
drawSelector()
// Draw Main Gameboard
let mainTable = document.createElement("table");
mainTable.setAttribute('class', 'mainTable')
mainDiv.append(mainTable)
function drawBoard() {
for (i=0; i<gameboard.length; i++){
let row = document.createElement("tr")
mainTable.append(row)
for (j=0; j<gameboard[i].length; j++){
let outerCell = document.createElement('td')
outerCell.setAttribute('class', 'outerCell')
row.append(outerCell)
let innerCell = document.createElement('div')
innerCell.setAttribute('class', 'innerCell')
innerCell.classList.add(gameboard[i][j])
innerCell.setAttribute('innerHTML', gameboard[i][j])
outerCell.append(innerCell)
}
}
};
drawBoard()
function validateRadio() {
let ele = document.getElementsByName('gameType');
for(i = 0; i < ele.length; i++) {
if(ele[i].checked){
gameType = (ele[i].value)
beginGame()
}
}
};
function beginGame() {
if (gameType == "1PEasy"){
itsAOnePlayerGame = true
resetBoard()
onePlayerPickSides()
play1PGame()
}
else if (gameType == "1PHard"){
itsAOnePlayerGame = true
resetBoard()
onePlayerPickSides()
play1PGame()
}
else if (gameType == "2P"){
itsAOnePlayerGame = false
resetBoard()
twoPlayerPickSides()
play2PGame()
}
};
function resetBoard() {
playerOneTurn = true
isThereAWinner = false
gameboard = [
[1,2,3,4,5,6,7],
[8,9,10,11,12,13,14],
[15,16,17,18,19,20,21],
[22,23,24,25,26,27,28],
[29,30,31,32,33,34,35],
[36,37,38,39,40,41,42]
];
}
function swapTurns() {
selectorTable.innerHTML = ""
drawSelector()
playerOneTurn = !playerOneTurn
document.getElementsByName("announcements")[0].innerHTML = "Current Player: " + whosPlaying() + " "
};
// GAMEPLAY
function playerSelects2P() {
findAvailableSpots()
// put an eventListener here?
columnPick = prompt(whosPlaying() + ', choose which column 1-7')
if (availableSpots.includes(parseInt(columnPick)))
{console.log(columnPick)}
else {
alert("not available")
playerSelects2P()}
};
function playerSelects1P() {
if (whosPlaying() == playerTwo) {
findAvailableSpots()
columnPick = availableSpots[Math.floor(Math.random() * availableSpots.length)]
return
}
else {playerSelects2P()}
};
function whosPlaying() {
if (playerOneTurn) {
return "Yellow"
} else {
return "Red"
}
};
// starts from the bottom row and claims spot when there it is a number (unoccupied)
function claimSpot(){
findAvailableSpots()
if (availableSpots.includes(indexPick+1)) {
let i;
for (i = 5; i > -1; i--)
{if (Number.isInteger(gameboard[i][indexPick])) {
gameboard[i].splice((indexPick), 1, whosPlaying())
mainTable.innerHTML = ""
drawBoard()
checkForWinners()
// do I need to put some sort of delay here for it not to go to swap turns right away?
swapTurns()
return
}
}
}
else {
console.log(availableSpots)
alert("Forbidden")
}
};
// if there is a string in row[0], that column is no longer available.
// the cells are numbered from 1 to 7, not per index so you need to add one to indexPick to identify
function findAvailableSpots() {
availableSpots = gameboard[0].filter(x => Number.isInteger(x) == true)
};
function checkForWinners() {
horizontalCheck()
verticalCheck()
downrightCheck()
uprightCheck()
}
// WIN CHECKERS
// a forloop evaluates a section of the matrix, moving through it and seeing if the 3 ahead match.
// it stops before going out of bounds
function findFour(w,x,y,z) {
// Checks first cell against current player and all cells match that player
return ((w == whosPlaying()) && (w === x) && (w === y) && (w === z));
};
function winDeclared() {
isThereAWinner = true
alert("winner")
document.getElementsByName("announcements")[0].innerHTML = whosPlaying() + " wins! "
// this does not show, it snaps to swap places
};
function uprightCheck() {
for (r=5; r>2; r--) {
for (c=0; c<4; c++){
if (findFour(gameboard[r][c], gameboard[r-1][c+1], gameboard[r-2][c+2], gameboard[r-3][c+3])) {
winDeclared()
return
}
}
}
};
function downrightCheck() {
for (r=0; r<3; r++) {
for (c=0; c<4; c++){
if (findFour(gameboard[r][c], gameboard[r+1][c+1], gameboard[r+2][c+2], gameboard[r+3][c+3])) {
winDeclared()
return
}
}
}
};
function verticalCheck() {
for (r=5; r>2; r--) {
for (c=0; c<7; c++){
if (findFour(gameboard[r][c], gameboard[r-1][c], gameboard[r-2][c], gameboard[r-3][c])) {
winDeclared()
return
}
}
}
};
function horizontalCheck() {
for (r=0; r<6; r++) {
for (c=0; c<4; c++){
if (findFour(gameboard[r][c], gameboard[r][c+1], gameboard[r][c+2], gameboard[r][c+3])) {
winDeclared()
return
}
}
}
};
When you manipulate the DOM, the operation itself is syncrhonous but the browser decides when the user will actually see the changes. Sometimes, the broswer will not have time to redraw before the prompt appears. To get around this, you can wrap the alert in a setTimeout() to delay the alert.
setTimeout(
function() {
alert("winner")
}, 10)
I've created an interactive quiz and created functions that showed the next question and goes back to the previous questions. My problem is that when it changes questions, the information I input in is erased. For example Question 1 and 2, I would write in an animal and then I go onto Question 3, but when I go back to Question 1 and 2, the animals I wrote in are gone. Same with the radio choices. Sorry if code is tedious, I'm just starting. Any tips on how I can fix that?
Quiz link: https://repl.it/GSiI/latest
function initialize()
{
questionList = document.getElementsByClassName("questions");
quizOutput = document.getElementById("showQuiz");
beginBtn = document.getElementById("initiate");
button = document.getElementById("button");
next = document.getElementById("next");
previous = document.getElementById("previous");
questionIndex = 0;
totalQuestions = questionList.length - 1;
}
function beginQuiz()
{
currentQuestion = questionList.item(questionIndex).innerHTML;
button.style.visibility = "visible";
quizOutput.innerHTML = currentQuestion;
beginBtn.style.display = "none";
quizOutput.style.display = "block";
}
function changeQuestion(factor)
{
if(factor == -1 && questionIndex > 0)
{
questionIndex--;
currentQuestion = questionList.item(questionIndex).innerHTML;
quizOutput.innerHTML = currentQuestion;
}
if(factor == 1 && questionIndex < totalQuestions)
{
questionIndex++;
currentQuestion = questionList.item(questionIndex).innerHTML;
quizOutput.innerHTML = currentQuestion;
}
}
function writeNumber(el, num)
{
input = document.getElementById(el);
if(input.value.length < 2)
{
input.value += num;
}
}
function clearAnswer(el)
{
document.getElementById(el).value = "";
}
function takeValues()
{
var x = document.getElementById("frm1");
var text = "";
var i;
for (i = 0; i < x.length ;i++)
{
text += x.elements[i].value + "<br>";
}
document.getElementById("demo").innerHTML = text;
}
Its because every time you load a different question, you are actually removing the DOM elements without saving them. When you return to the previous questions, you aren't loading their values from anywhere. You need to build a state into your app for it to work.
I will suggest using javascript FormData object to get entries of input values .
First removing all the "form" elements from your html but keep their inner elements.
Then make "showQuiz" div into a form element.
Then create 1 single form ( I set this form to have id "form1") to contain all the questions and the submit button .
Then modify the javascript function changeQuestion as follow:
function changeQuestion(factor)
{
var myform = document.getElementById("showQuiz");
var fdata = new FormData(myform);
var storeForm = document.getElementById("form1");
var sdata = new FormData(storeForm);
for (var pair of fdata.entries()) {
var elem = document.getElementsByName(pair[0])[0];
storeForm.elements[pair[0]].value = pair[1];
}
if(factor == -1 && questionIndex > 0)
{
questionIndex--;
currentQuestion = questionList.item(questionIndex).innerHTML;
quizOutput.innerHTML = currentQuestion;
}
if(factor == 1 && questionIndex < totalQuestions)
{
questionIndex++;
currentQuestion = questionList.item(questionIndex).innerHTML;
quizOutput.innerHTML = currentQuestion;
}
myform = document.getElementById("showQuiz");
storeForm = document.getElementById("form1");
fdata = new FormData(myform);
sdata = new FormData(storeForm);
for (var pair of sdata.entries()) {
var fld = myform.elements[pair[0]];
if (fld)
fld.value = pair[1];
}
}
If I have a paragraph
<p id=peed></p>
and I have a button which on click runs this function:
<script>
var x = 'welcome'
function onButtonClick() {
document.getElementById('peep').innerHTML = [x];
}
</script>
This swaps the peep paragraph with variable x.
How, using if statements, would I make the same button when clicked a second time reverse this an replace x with the paragraph peep again?
Function like that?
<script>
function myFunction()
{
var tmp = document.getElementById("peep").innerHTML;
if(tmp === 'x') {
document.getElementById("peep").innerHTML = 'y';
}
else {
document.getElementById("peep").innerHTML = 'x';
}
}
</script>
With ES6, you could use the destructuring assignment for swapping the variable and the actual text.
var a = 5, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 5
Implementation
var x = 'welcome';
function peep() {
[document.getElementById("peep").innerHTML, x] = [x, document.getElementById("peep").innerHTML];
}
<button onclick="peep()">peep</button>
<div id="peep">some text</div>
You first need to get the original content:
var originalPeep = document.getElementById("peep").innerHTML;
You need to have a variable to store whether it has been replaced or not:
var replaced = false;
Now in your function, you need to check the value of replaced and replace it accordingly (and reset our replaced marker):
function myFunction() {
if (replaced) {
document.getElementById("peep").innerHTML = originalPeep;
replaced = false;
} else {
document.getElementById("peep").innerHTML = x;
replaced = true;
}
}
So in total that becomes:
var originalPeep = document.getElementById("peep").innerHTML;
var replaced = false;
function myFunction() {
if (replaced) {
document.getElementById("peep").innerHTML = originalPeep;
replaced = false;
} else {
document.getElementById("peep").innerHTML = x;
replaced = true;
}
}
Swapping values is tricky: you need to store the current value in a temporary variable, replace the current value with the new one, then assign the temporary value to the variable holding new one.
document.querySelector("button").onclick = myFunction;
var x = 'welcome';
function myFunction()
{
var peep = document.getElementById("peep");
var tmp = peep.innerHTML;
peep.innerHTML = x;
x = tmp;
}
<div id="peep">peep</div>
<button>swap</button>
var x = 'welcome'
var peeepText = '';
function myFunction() {
if (!peeepText) {
peeepText = document.getElementById("peep").innerHTML;
document.getElementById('peep').innerHTML = [x];
} else {
document.getElementById('peep').innerHTML = peepText;
peepText = '';
}
}
You could try doing this with an array an count as well.
var x = [];
x[0] = 'welcome';
x[1] = 'googbye';
var count = 1;
function myFunction()
{
document.getElementById('peep').innerHTML = x[count%2];
count++;
}
First of all, I'm not so sure about how to ask this question. I have an array of elements:
var buttons = publishedWork.getElementsByTagName('button');
and what I what is that each button changes its content from ▶ to ◼ and viceversa by clicking. The thing is that I don't know how many buttons will be in total, I intend to do it with a for:
var currentButton;
for (var i = buttons.length; i;) {
buttons[--i].onclick = function() {
if (currentButton === buttons[i]) {
currentButton.textContent = '▶';
currentButton = null;
} else {
currentButton = buttons[i];
currentButton.textContent = '◼';
}
}
}
But what this code does is that, no matter what button I click, it always changes the content of the first button, from which I get that is the expression buttons[i] the one that is stored in currentButton, and not the reference to the button itself.
So my question is:
is this a case to resolve by closures (a topic that I'm just beginning to grasp) or is there another solution?
Unless I'm mistaken, it looks like the common "defining an index-dependent function in a loop" issue. When the onclick function is invoked, it accesses the i variable. But when that happens, i has been all the way through the loop, and is 0. So all the click-handlers just see i == 0
To solve it, you can create a function that, in turn, creates the click-handler:
var currentButton;
function createClickHandler(index) {
var button = buttons[index];
return function() {
if (currentButton === button) {
currentButton.textContent = '▶';
currentButton = null;
} else {
currentButton = button;
currentButton.textContent = '◼';
}
};
}
for (var i = buttons.length; i;) {
buttons[--i].onclick = createClickHandler(i);
}
Edit: Or use Diode's suggestion :)
I was focused on the "index in a closure" problem, but Diode's answer is much cleaner, and the better way to handle it
Use this or event.currentTarget inside click handler
....
buttons[--i].onclick = function(event) {
// both `this` and `event.currentTarget` gives the clicked button here
}
....
.
var currentButton;
for (var i = buttons.length; i;) {
buttons[--i].onclick = function() {
if (currentButton === this) {
...
currentButton = null;
} else {
currentButton = this;
...
}
}
}
and in your else condition you have to reset the current button first.
var currentButton;
for (var i = buttons.length; i;) {
buttons[--i].onclick = function(event) {
if (currentButton === this) {
currentButton.textContent = '▶';
currentButton = null;
} else {
if(currentButton){
currentButton.textContent = '▶';
}
currentButton = this;
currentButton.textContent = '◼';
}
}
}