I have a function that takes text and renders it to the DOM character by character to create a typewriter effect. To accomplish this, the function keeps calling itself over and over again for each character.
Sometimes I want to jump to another level and so my props.chapter changes and my textArray gets updated. However the function that was invoked before the props and textArray changed still sees the old values with each successive time it calls itself.
If I trigger the function a second time after the prop changes, the two invokations run simultaneously with one display the old data and one display the new data.
I am using React with Hooks.
Here is my function:
const typewriter = () => {
console.log(props.chapter); //will always show the props at invokation
sContents = [''];
iRow = Math.max(0, iIndex - iScrollAt);
while (iRow < iIndex) {
sContents[iIndex] += textArray[iRow++];
sContents[iIndex] = '';
}
//This is the text I render to the DOM later
setText((text) => {
text[iIndex] = <p key={`line-${iIndex}`}>{sContents[iIndex] + textArray[iIndex].substring(0, iTextPos)}<span className='caret'></span></p>;
return [...text];
});
if (iTextPos++ == iArrLength) {
iTextPos = 0;
iIndex++;
if (iIndex != textArray.length) {
setSFXon(false);
setTimeout(() => {
//Clear carat from previous line
setText((text) => {
if(text[iIndex -1]){
text[iIndex-1] = <p key={`line-${iIndex-1}`}>{sContents[iIndex-1] + textArray[iIndex-1].substring(0, iArrLength)}</p>;
}
text[iIndex] = <p key={`line-${iIndex}`}><span className='caret'></span></p>
return [...text];
});
//Write next line
iArrLength = textArray[iIndex].length; //Next line's length
//==================
//RECURSIVE CALL HERE
//==================
typewriter()
}, 1500);
}else{
//Make last line carat blink
setText((text) => {
text[iIndex-1] = <p key={`line-${iIndex-1}`}>{sContents[iIndex-1] + textArray[iIndex-1].substring(0, iArrLength)}<span className='caret blink'></span></p>;
return [...text];
});
}
} else {
//==================
// RECURSIVE CALL HERE
//==================
setTimeout(() => typewriter(), iSpeed);
}
}
Related
I am writing a function of a game:
function Game(){
while(true){
***
for(var i = 0; i < level; i++){
var color;
$(".btn").on("click", function(event) {
ButtonClickResponse(this.id);
color = this.id;
});
if(colorsOrder[i] != color){
GameOver();
return;
}
}
***
}
}
the "if statement" in the loop of function runs and increments "i" immediately many times when loop is started and doesnt wait for an above event to finish.
I searched for solving with "async await" and "promise" in google and stackoverflow, but didn't really understand how it worked so couldn't implemet it in my code.
This should work, although I didn't test it and you do things not in javascript way
async function Game() {
while (true) {
var level = 1;
$("#level-title").text("Level " + level);
var colorsOrder = [];
var randColor = GenerateRandomSquareColor();
colorsOrder.push(randColor);
ButtonClickResponse(randColor);
for (var i = 0; i < level; i++) {
var color;
// await for the click event
const event = await waitForClick($(".btn"));
// do stuff with the result
ButtonClickResponse(event.target.id);
color = event.target.id;
if (colorsOrder[i] != color) {
GameOver();
return;
}
level++;
}
}
}
function waitForClick(element) {
// create new promise
return new Promise((resolve) => {
const handler = function (event) {
// when button is clicked remove listener
element.off("click", handler);
// and resolve the promise
resolve(event);
};
// listen for click
element.on("click", handler);
});
}
Here is the link to my repo's github page, so you can properly see what I mean.
I am currently having an issue with my triviaGame function when trying to make it recursive, but it's sort of "backfiring" on me in a sense.
You'll notice after you answer the first question, everything seems fine. It goes to the next question fine. After that though, it seems like the iterations of it double? The next answer it skips 2. After that, 4. And finally the remaining 2 (adding up to 10, due to how I am iterating over them).
How might I be able to correctly iterate over a recursive function, so it correctly calls all 10 times, and then returns when it is done?
Been struggling with this for hours, and just can't seem to get it to work. My javascript code is below, sorry for any headaches that it may give you. I know I make some questionable programming decisions. Ignore some of the commented out stuff, it's not finished code yet. I'm a beginner, and hope that once I learn what's going on here it will stick with me, and I don't make a stupid mistake like this again.
const _URL = "https://opentdb.com/api.php?amount=1&category=27&type=multiple";
const _questionHTML = document.getElementById("question");
const _answerOne = document.getElementById("answer-1");
const _answerTwo = document.getElementById("answer-2");
const _answerThree = document.getElementById("answer-3");
const _answerFour = document.getElementById("answer-4");
const btns = document.querySelectorAll("button[id^=answer-]");
var runCount = 1;
var correct = 0;
// Credits to my friend Jonah for teaching me how to cache data that I get from an API call.
var triviaData = null;
async function getTrivia() {
return fetch("https://opentdb.com/api.php?amount=1&category=27&type=multiple")
.then((res) => res.json())
.then((res) => {
triviaData = res;
return res;
});
}
// anywhere I want the trivia data:
// const trivia = await getTrivia() --- makes the call, or uses the cached data
const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
};
async function triviaGame() {
const trivia = await getTrivia();
async function appendData() {
let totalAnswers = [
...trivia.results[0].incorrect_answers,
trivia.results[0].correct_answer,
];
// Apparently I need 2 different arrays to sort them because array variables are stored by reference? Learn something new everyday I guess.
let totalAnswers2 = [...totalAnswers];
let sorted = shuffleArray(totalAnswers2);
// Ensures the proper symbol shows instead of the HTML entities
const doc = new DOMParser().parseFromString(
trivia.results[0].question,
"text/html"
);
_questionHTML.textContent = doc.documentElement.textContent;
console.log(trivia.results[0].correct_answer, "- Correct Answer");
// Appends info to the DOM
_answerOne.textContent = sorted[0];
_answerTwo.textContent = sorted[1];
_answerThree.textContent = sorted[2];
_answerFour.textContent = sorted[3];
}
async function checkAnswer() {
btns.forEach((btn) => {
btn.addEventListener("click", (event) => {
console.log(runCount);
if (event.target.textContent === trivia.results[0].correct_answer) {
event.target.style.backgroundColor = "#52D452";
// Disables all buttons after one has been clicked.
btns.forEach((btn) => {
btn.disabled = true;
});
setTimeout(() => {
if (runCount === 10) {
return;
}
runCount++;
correct++;
btns.forEach((btn) => {
btn.disabled = false;
});
btn.style.backgroundColor = "";
document.getElementById(
"amount-correct"
).textContent = `${correct}/10`;
triviaGame();
}, 2000);
} else {
event.target.style.backgroundColor = "#FF3D33";
btns.forEach((btn) => {
btn.disabled = true;
});
// document.getElementById("correct-text").textContent =
// trivia.results[0].correct_answer;
// document.getElementById("correct-answer").style.visibility =
// "visible";
setTimeout(() => {
if (runCount === 10) {
return;
}
// document.getElementById("correct-answer").style.visibility =
// "hidden";
btns.forEach((btn) => {
btn.disabled = false;
btn.style.backgroundColor = "";
});
runCount++;
triviaGame();
}, 3500);
}
});
});
}
checkAnswer();
appendData();
}
triviaGame();
Any/All responses are much appreciated and repsected. I could use any help y'all are willing to give me. The past 6 hours have been a living hell for me lol.
It's skipping questions once an answer is clicked because every time a button is clicked, another event listener is added to the button, while the original one is active:
On initial load: triviaGame() runs which makes checkAnswer() run which adds event listeners to each of the buttons.
Event listeners on buttons: 1.
Answer button is clicked, triviaGame() runs which makes checkAnswer() run which adds event listeners to each of the buttons.
Event listeners on buttons: 2.
Answer button is clicked, triviaGame() runs twice (from the 2 listeners attached) which makes checkAnswer() run twice where both invocations adds event listeners to each of the buttons.
Event listeners on buttons: 4.
etc.
To fix this, I moved the content of checkAnswer() outside of any functions so it only ever runs once. However, doing this, it loses reference to the upper scope variable trivia. To resolve this, I used the triviaData variable instead which checkAnswer() would have access to and I change references in appendData() to match this. Now, triviaGame() function only exists to call appendData() function inside it; there is little point in this so I merge the two functions together into one function, instead of two nested inside each other.
const _URL = "https://opentdb.com/api.php?amount=1&category=27&type=multiple";
const _questionHTML = document.getElementById("question");
const _answerOne = document.getElementById("answer-1");
const _answerTwo = document.getElementById("answer-2");
const _answerThree = document.getElementById("answer-3");
const _answerFour = document.getElementById("answer-4");
const btns = document.querySelectorAll("button[id^=answer-]");
var runCount = 1;
var correct = 0;
// Credits to my friend Jonah for teaching me how to cache data that I get from an API call.
var triviaData = null;
async function getTrivia() {
return fetch("https://opentdb.com/api.php?amount=1&category=27&type=multiple")
.then((res) => res.json())
.then((res) => {
triviaData = res;
return res;
});
}
// anywhere I want the trivia data:
// const trivia = await getTrivia() --- makes the call, or uses the cached data
const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
};
async function appendData() {
triviaData = await getTrivia();
let totalAnswers = [
...triviaData.results[0].incorrect_answers,
triviaData.results[0].correct_answer,
];
// Apparently I need 2 different arrays to sort them because array variables are stored by reference? Learn something new everyday I guess.
let totalAnswers2 = [...totalAnswers];
let sorted = shuffleArray(totalAnswers2);
// Ensures the proper symbol shows instead of the HTML entities
const doc = new DOMParser().parseFromString(
triviaData.results[0].question,
"text/html"
);
_questionHTML.textContent = doc.documentElement.textContent;
console.log(triviaData.results[0].correct_answer, "- Correct Answer");
// Appends info to the DOM
_answerOne.textContent = sorted[0];
_answerTwo.textContent = sorted[1];
_answerThree.textContent = sorted[2];
_answerFour.textContent = sorted[3];
}
btns.forEach((btn) => {
btn.addEventListener("click", (event) => {
console.log(runCount);
if (event.target.textContent === triviaData.results[0].correct_answer) {
event.target.style.backgroundColor = "#52D452";
// Disables all buttons after one has been clicked.
btns.forEach((btn) => {
btn.disabled = true;
});
setTimeout(() => {
if (runCount === 10) {
return;
}
runCount++;
correct++;
btns.forEach((btn) => {
btn.disabled = false;
});
btn.style.backgroundColor = "";
document.getElementById(
"amount-correct"
).textContent = `${correct}/10`;
appendData();
}, 2000);
} else {
event.target.style.backgroundColor = "#FF3D33";
btns.forEach((btn) => {
btn.disabled = true;
});
// document.getElementById("correct-text").textContent =
// trivia.results[0].correct_answer;
// document.getElementById("correct-answer").style.visibility =
// "visible";
setTimeout(() => {
if (runCount === 10) {
return;
}
// document.getElementById("correct-answer").style.visibility =
// "hidden";
btns.forEach((btn) => {
btn.disabled = false;
btn.style.backgroundColor = "";
});
runCount++;
appendData();
}, 3500);
}
});
});
appendData();
<div id="amount-correct"></div>
<h1 id="question"></h1>
<button id="answer-1"></button>
<button id="answer-2"></button>
<button id="answer-3"></button>
<button id="answer-4"></button>
Promise.all([
seperatingDMCM(),
compileDirectMessage(),
renderingResult(),
addResultButtonListener(),
]);
I have a progress bar in my UI and I have 4 functions mentioned above each of which returns a Promise. I have a loop inside the first function seperatingDMCM(), that will handle all the my data. I want to increment the progress bar with each increment of the loop. Meaning each loop iteration should be async, so the UI will update and only afterwards the loop will iterate. When the loop has ended I want to return a promise so that the other functions will begin to execute. I am facing an issue that progress bar is not working as it suppose to and is immediately being invoked when seperatingDMCM() returns the promise () and not asynchronously. This progress bar is immediately being updated to 100% on the next page instead of live updates with small increment on the current page.
This is my updateUI function:
function startProgressBar(i, arr) {
return new Promise((resolve) => {
setTimeout(() => {
i = (i * 100) / arr.length;
console.log(i);
let elem = document.getElementById("progressBar");
if (i < 100) {
elem.innerText = i + "%";
elem.style.width = i + "%";
elem.innerHTML = i + "%";
resolve();
}
}, 0);
});
}
This is my first function where I want to update the UI per loop iteration
function seperatingDMCM() {
const contentType = "Content type";
return new Promise((resolve) => {
rowObject.forEach(async (row, index) => {
const creatingInts = () => {
console.log("CreatedInt at", index);
row["Date created (UTC)"] = ExcelDateToJSDate(
row["Date created (UTC)"]
);
if (
row[contentType] !== "DM" &&
row.hasOwnProperty("Falcon user name")
) {
publicCommentsCount++;
interaction = { row : row};
compiledInteractions.push(interaction);
interaction = {};
} else {
dmData.push(row);
}
};
startProgressBar(index, rowObject).then(creatingInts());
});
quickSort(dmData, "Falcon URL");
console.log("SORTED", dmData);
console.log(workbook);
console.log("Rows", rowObject);
resolve();
});
}
I need to run the below code and after 10 seconds the SetInteraval function to be stopped but in the same time to assure that the full word has been executed.
The code I had written:
var word = "I love JS More than any Programming Language in the world!";
var counter = 0;
var autoTyping = setInterval(function() {
var h3 = document.getElementById("myh3");
h3.innerText = word.substring(0, counter);
counter++;
if (counter > word.length) {
counter = 0;
}
}, 100);
setTimeout(function() {
clearInterval(autoTyping);
}, 5000);
So I need after 5 seconds this code stop and this happened but it can be stopped without ensuring that full word "Variable word" has been totally completed written on the DOM.
I assume that you want to print the word variable to h3 element, per letter, and stop it after 5s AND the variable was fully-typed.
Here's my solution with recursive approach:
[UPDATE]
Added typing loop with timeout stopper
// word to type
var _word = "I love JS More than any Programming Language in the world!"
// target element's id
var _target = 'myh3'
// time to fully-typed the word
var _time = 5000 // ms
// speed is depend on _time and _word's length
var _speed = _time/_word.length
// your auto-typing stopper
var _timeout = 10000 // ms
// auto-typing function
function autoType (word, target, speed, timeout) {
var element = document.getElementById(target)
var counter = 0
var stopped = false
function typing(){
if(counter < word.length){
element.innerHTML += word[counter]
counter++
setTimeout(function(){
// recursive call
typing()
}, speed)
}else{
// check if you want it to stop
if(stopped === false){
// ok. you don't want it to stop now. reset counter
counter = 0
// reset the element if you want it too
element.innerHTML = ''
// start it again
typing()
}else{
// console.log('auto-typing is done')
}
}
}
// timeout is required. you dont want a infinite loop, right?
if(timeout){
typing()
setTimeout(function(){
stopped = true
}, timeout)
}
}
// execute it
autoType(_word, _target, _speed, _timeout)
body {background: white}
<h3 id="myh3"></h3>
Well, you are almost there. In your setInterval callback add a line where you clear the interval whenever the word length is reached.
In the setTimeout callback first check if the innerText value of your element is equal to the word. This way you can see if the full sentence has been printed out and only stop if it is so. Otherwise the setInterval will just continue to run until the word length is reached.
var h3 = document.getElementById("myh3");
var word = "I love JS More than any Programming Language in the world!";
var counter = 0;
var autoTyping = setInterval(function() {
h3.innerText = word.substring(0, counter);
counter++;
if (counter >= word.length) {
clearInterval(autoTyping);
counter = 0;
}
}, 100);
setTimeout(function() {
if (h3.innerText === word) {
clearInterval(autoTyping);
}
}, 5000);
you can just have the clearinterval in your if statement, without the need to have setTimeout function:
var word = "I love JS More than any Programming Language in the world!";
var counter = 0;
var h3 = document.getElementById("myh3");
var autoTyping = setInterval(function() {
h3.innerText = word.substring(0, counter);
counter++;
if (counter > word.length) {
counter = 0;
//clearInterval(autoTyping);
}
}, 100);
setTimeout(function() {
if(h3.innerHTML !== word) {
h3.innerHTML = word;
}
clearInterval(autoTyping);
}, 10000);
<div id="myh3">
</div>
I really like to use ES6 generator functions, when it comes to intervals. They make the code much cleaner.
Here's an example of a reusable typewriter function, that takes the element, the word and the interval; and returns a stop function:
function typewriter(element, word, interval){
let stopped = false
const iterator = (function*() {
//This try..finally is not necessary, but ensures that typewriter stops if an error occurs
try{
while(!stopped){
for(let i=0; i<word.length; i++){
element.innerText = word.substring(0, i);
yield
}
}
}finally{
clearTimeout(autoTyping)
}
})()
const autoTyping = setInterval(() => iterator.next(), interval);
iterator.next()
return function stop(){
stopped = true
}
}
const word = "I love JS More than any Programming Language in the world!";
const h3 = document.getElementById("myh3");
const stop1 = typewriter(h3, word, 100)
setTimeout(stop1, 10000)
const secondh3 = document.getElementById("my2ndh3");
const stop2 = typewriter(secondh3, word, 100)
//Even if stopped immediately, it types the full sentence
stop2()
<h3 id="myh3"></h3>
<h3 id="my2ndh3"></h3>
To return results, you can even promisify this function, that has the advantage of having an exception channel for asynchronous errors:
function typewriter(element, word, interval){
let stopped = false
const promise = new Promise((resolve, reject) => {
const iterator = (function*() {
try{
while(!stopped){
for(let i=0; i<word.length; i++){
element.innerText = word.substring(0, i);
yield
}
}
resolve()
}catch(e){
reject(e)
}finally{
clearTimeout(autoTyping)
}
})()
const autoTyping = setInterval(() => iterator.next(), interval);
iterator.next()
})
promise.stop = function stop(){
stopped = true
}
return promise
}
const word = "I love JS More than any Programming Language in the world!";
const h3 = document.getElementById("myh3");
const promise1 = typewriter(h3, word, 100)
promise1.then(() => console.log('1st example done'))
setTimeout(promise1.stop, 10000)
typewriter(null, word, 100) //Cause error with null element
.catch(e => console.error('2nd example failed:', e.stack))
<h3 id="myh3"></h3>
I'm trying to add a string to a html element every 500ms using a for loop to pass the string to a function which updates the target element.
I'm not sure if i'm approaching this the right way or if it's possible as it just displays the strings all at once rather than every 500ms.
The desired effect is the strings displays as if someone is typing.
The code is below and here is a jsFiddle.
var content = "Hello, Universe!";
var split = content.split("");
var target = document.getElementsByClassName('place-here');
for (i = 0; i < split.length; i++) {
addChar(split[i]);
}
function addChar(char) {
if (timer) {
clearTimeout(timer);
}
var timer = setTimeout(function() {
target[0].innerHTML += char;
}, 500);
}
Just a proposal without setTimeout, but with setInterval and some other changes.
var content = "Hello, Universe!",
target = document.getElementById('ticker'),
i = 0,
timer = setInterval(addChar, 500);
function addChar() {
if (i < content.length) {
target.innerHTML += content[i];
i++;
} else {
clearTimeout(timer);
}
}
<div id="ticker"></div>
The problem is that all timeout functions starts at the same time (in 500ms). You can compute different timeouts by multiplying interval with the index of char in array:
for (i = 0; i < split.length; i++) {
addChar(split[i], i);
}
function addChar(char, i) {
setTimeout(function () {
target[0].innerHTML += char;
}, 500 * i);
}
You can mix setInterval and shift to do this :
var target = document.getElementsByClassName('place-here');
function display(element, string, timer) {
var split = string.split("");
var interval = setInterval(function() {
element.innerHTML += split.shift()
if (split.length == 0) {
clearInterval(interval);
}
}, timer)
}
display(target[0], "Hello, Universe!", 500)
I made you a working javascript code that you can paste in your fiddle. Your problem was that you call the setTimetout all at the same time so after 500 milliseconds everything executes instead of waiting on the other one to finish
var content = "Hello, Universe!";
var split = content.split("");
var target = document.getElementsByClassName('place-here');
console.log(split);
addChar(0);
function addChar(i) {
var currentChar = split[i];
console.log(i);
console.log(currentChar);
setTimeout(function() {
if(typeof currentChar !== 'undefined'){
target[0].innerHTML += currentChar;
addChar(i+1);
}
}, 1000);
}
Why not using setInterval() :
here is a fiddle
code here :
var content = "Hello, Universe!";
var split = content.split("");
var target = document.getElementsByClassName('place-here');
var length = split.length;
var count = 0;
var timer = setInterval(function() {
addChar(split[count]);
count++;
if(count>=length) clearInterval(timer);
}, 500);
function addChar(char) {
target[0].innerHTML += char;
}
<div class="place-here">
</div>
I made a few changes in your jsfiddle: https://jsfiddle.net/vochxa3f/10/
var content = "Hello, Universe!";
var split = content.split("");
var target = document.getElementsByClassName('place-here');
function addChar(str, i) {
if(i < str.length) {
target[0].innerHTML += str[i];
setTimeout(addChar, 500, str, ++i);
}
}
setTimeout(addChar(content, 0), 500);
The addChar function call itself with setTimeout() incrementing the variable i so in the next time it'll get another character from content.
Note that setTimeout() first argument is the reference to the function, in this case only "addChar" without "()". The arguments for the function stars at 3rd parameter and forth.