Go to nextURL on local server - javascript

I need to go to the next URL after a correct answer on a quiz. I have an assignment where I'm creating a Quiz game with questions from a server at the university. When the person is correct the game gets the next question on the server with a XMLHttpRequest.
How can I somehow us a 'nextURL' here or is there no such term?
function Question () {
let quizQuestion = new window.XMLHttpRequest()
quizQuestion.open('GET', 'http://vhost3.lnu.se:20080/question/1')
quizQuestion.onload = function () {
let ourData = JSON.parse(quizQuestion.responseText)
let questionDiv = document.querySelector('#question')
questionDiv.innerText = ourData.question
}
quizQuestion.send()
answer()
}
function answer () {
let quizQuestion = new window.XMLHttpRequest()
let answerDiv = document.querySelector('#answer')
let button = document.createElement('button')
button.type = 'button'
button.setAttribute('id', 'send')
button.innerText = 'Answer'
answerDiv.appendChild(button)
button.addEventListener('click', function () {
quizQuestion.open('POST', 'http://vhost3.lnu.se:20080/answer/1')
quizQuestion.setRequestHeader('Content-type', 'application/json')
quizQuestion.send(JSON.stringify({answer: inputText.value}))
quizQuestion.onreadystatechange = function () {
console.log(quizQuestion.response)
let ourAnswer = JSON.parse(quizQuestion.responseText)
let answerDiv = document.querySelector('#answer')
answerDiv.innerText = ourAnswer.message
}
})
}
So if the value in ({answer: inputText.value}) is correct I want to go to the next question, which in this case is in quizQuestion.open('GET', 'http://vhost3.lnu.se:20080/question/21')

Based on what you've written, it looks like "next URL" at any given moment would be next in a list that you've been given, and it's up to you to figure out how to retrieve the appropriate one after a correct answer.
We'll assume the question numbers in your assignment are non-sequential (moving from question 1 to question 21 in your example), and that no questions repeat. Is there a list of the questions in the order you need on the server? If the list is in an array, can you access it based on the index of the current question?
If not, assuming you already know the list of questions in the desired order, you can do this in your own code. Suppose you put your question numbers into an array, and store the current question number, like so:
let questionNums = [1,21,14,9,6,23]
let currQuestionNum = questionNums[0]
This lets you concatenate the desired question number onto your base URL as
'http://vhost3.lnu.se:20080/question/' + currQuestionNum.toString().
Then, when you've checked if the answer is correct, you can move to the next question in the array:
if (questionNums.indexOf(currQuestionNum)+1 != questionNums.length){
currQuestionNum = questionNums[questionNums.indexOf(currQuestionNum)+1]
}
else{
//end the quiz
}
To use this with the concatenation example above, you'll need to modify your Question and answer functions to accept question numbers as parameters:
function Question (questionNum) {
let quizQuestion = new window.XMLHttpRequest()
quizQuestion.open('GET', 'http://vhost3.lnu.se:20080/question/'+questionNum)
quizQuestion.onload = function () {
let ourData = JSON.parse(quizQuestion.responseText)
let questionDiv = document.querySelector('#question')
questionDiv.innerText = ourData.question
}
quizQuestion.send()
answer(questionNum)
}
function answer (questionNum) {
let quizQuestion = new window.XMLHttpRequest()
let answerDiv = document.querySelector('#answer')
//Local answerNum variable
let answerNum = questionNum
let button = document.createElement('button')
button.type = 'button'
button.setAttribute('id', 'send')
button.innerText = 'Answer'
answerDiv.appendChild(button)
button.addEventListener('click', function () {
quizQuestion.open('POST', 'http://vhost3.lnu.se:20080/answer/'+answerNum)
quizQuestion.setRequestHeader('Content-type', 'application/json')
quizQuestion.send(JSON.stringify({answer: inputText.value}))
quizQuestion.onreadystatechange = function () {
console.log(quizQuestion.response)
let ourAnswer = JSON.parse(quizQuestion.responseText)
let answerDiv = document.querySelector('#answer')
answerDiv.innerText = ourAnswer.message
}
})
}
Note the local answerNum variable - this is added so that, if quesitonNum changes before the anonymous function is called on a click event, the value won't be affected.

Related

I'm just starting with JS and I don't really understand how setTimeout works

I am developing a trivia page with a series of questions that I get through an API. When I give to start the game on the main page, I want to redirect me to a second page where the available categories will appear and when I click on them, the questions will appear with multiple answers.
Luckily I have managed to do all this, however, when I switch from one page to another I have not been able to get the categories to appear. They only appear if I am already on that page and I create a button to call the function that displays them.
I had thought of doing this function in this way so that it would switch me to the other page and then load the questions but I have not been able to get it to work.
Does anyone have a solution or know why it is not working?
function get_dataJson() {
let data_json = [];
fetch('https://opentdb.com/api.php?amount=10')
.then(response => response.json())
.then(data => data.results.forEach(element => {
data_json.push(element);
}));
return data_json;
}
let trivial_data = get_dataJson();
function startGame() {
let div_username = document.createElement("div");
let name = document.createElement("h2");
name.innerHTML = document.getElementById("name").value;
div_username.appendChild(name);
let categories = get_categories(trivial_data);
setTimeout("location.href='./game.html'", 1000);
setTimeout(function(){
document.getElementById('nav-game').appendChild(div_username);
show_categories(categories);
},2000);
}
function get_categories(trivial_data) {
let categories = [];
trivial_data.forEach(element => {
categories.push(element.category)
});
console.log(categories);
return categories;
}
function show_categories (categories) {
let div_categories = document.createElement("div");
let count = 0;
categories.forEach ( element => {
let element_category = document.createElement("button");
element_category.innerHTML = element;
element_category.id = count;
element_category.onclick = function() {
get_question(element_category.id);
};
count++;
div_categories.appendChild(element_category);
});
div_categories.className = 'categories';
document.getElementById("categories").appendChild(div_categories);
}
function get_question (pos) {
document.getElementById("categories").style.displays = 'none';
let question = document.createElement("h2");
question.innerHTML = trivial_data[pos].question;
let correct_answer = trivial_data[pos].correct_answer;
let incorrect_answers = trivial_data[pos].incorrect_answers;
let options = incorrect_answers.concat(correct_answer);
options = options.sort();
let div_choices = document.createElement("div");
options.forEach(element => {
let choice = document.createElement('button');
choice.innerHTML = element;
choice.id = 'true' ? element == correct_answer : 'false';
div_choices.appendChild(choice);
});
div_choices.className = 'answers';
document.getElementById("questions").appendChild(question);
document.getElementById("questions").appendChild(div_choices);
}
The first thing you should do is make sure you understand how to use fetch to get data asynchronously
async function get_dataJson() {
const response = await fetch('https://opentdb.com/api.php?amount=10');
const json = await response.json();
return json.results;
}
(async function(){
const trivial_data = await get_dataJson();
console.log(trivial_data);
})()
Then, rather than changing page, just stay on the same page and add/remove/hide elements as necessary - it looks like you have that bit sorted.

Can't add an event listener to an element from an API

I am building a trivia webapp using an API and i want to add an event listener to the button with the correct answer so that when the user clicks it it will display a message saying they're right and it'll get a new question.
Here's how the code looks:
function useApiData(triviaObj) {
let answers = sortArrayRandomly([
triviaObj.results[0].correct_answer,
triviaObj.results[0].incorrect_answers[0],
triviaObj.results[0].incorrect_answers[1],
triviaObj.results[0].incorrect_answers[2],
]);
document.querySelector("#category").innerHTML = `Category: ${triviaObj.results[0].category}`;
document.querySelector("#difficulty").innerHTML = `Difficulty: ${triviaObj.results[0].difficulty}`;
document.querySelector("#question").innerHTML = `Question: ${triviaObj.results[0].question}`;
document.querySelector("#answer1").innerHTML = `${answers[0]}`;
document.querySelector("#answer2").innerHTML = `${answers[1]}`;
document.querySelector("#answer3").innerHTML = `${answers[2]}`;
document.querySelector("#answer4").innerHTML = `${answers[3]}`;
let rightAnswer = triviaObj.results[0].correct_answer;
rightAnswer.addEventListener("click", correctAnswer);
console.log(answers);
}
function correctAnswer() {
alert("correct");
getTrivia();
}
It tells me that the AddEventListener is not a function, how can I fix this?
Use a loop to fill in the answer elements. In that loop you can check if the current answer is the correct answer and add the event listener.
answers.forEach((answer, i) => {
let button = document.querySelector(`#answer${i+1}`);
button.innerHTML = answer;
if (answer == triviaObj.results[0].correct_answer) {
button.addEventListener("click", correctAnswer);
} else {
button.removeEventListener("click", correctAnswer);
}
});
Event listeners go on DOM elements, not on pieces of data or other variables. Try finding the correct answer element and attaching the event listener to that:
const rightAnswer = triviaObj.results[0].correct_answer;
const matchingAnswer = answers.find((x) => x === rightAnswer);
const rightElement = Array.from(document.querySelectorAll('*[id^="answer"]'))
.find((el) => el.innerText.includes(rightAnswer))
rightElement.addEventListener("click", correctAnswer);
Assuming that triviaObj.results[0].correct_answer is a number representing the correct answer, then:
Replace
let rightAnswer = triviaObj.results[0].correct_answer;
with
let rightAnswer = document.querySelector(`#answer${triviaObj.results[0].correct_answer}`);
This is much more simple than Zan Anger's solution.

JavaScript - Issues recovering a map in an object after being saved in localStorage

I've been dealing with this for some time. I've a list of sections in which the user checks some checkboxes and that is sent to the server via AJAX. However, since the user can return to previous sections, I'm using some objects of mine to store some things the user has done (if he/she already finished working in that section, which checkboxes checked, etc). I'm doing this to not overload the database and only send new requests to store information if the user effectively changes a previous checkbox, not if he just starts clicking "Save" randomly. I'm using objects to see the sections of the page, and storing the previous state of the checkboxes in a Map. Here's my "supervisor":
function Supervisor(id) {
this.id = id;
this.verif = null;
this.selections = new Map();
var children = $("#ContentPlaceHolder1_checkboxes_div_" + id).children().length;
for (var i = 0; i < children; i++) {
if (i % 2 == 0) {
var checkbox = $("#ContentPlaceHolder1_checkboxes_div_" + id).children()[i];
var idCheck = checkbox.id.split("_")[2];
this.selections.set(idCheck, false);
}
}
console.log("Length " + this.selections.size);
this.change = false;
}
The console.log gives me the expected output, so I assume my Map is created and initialized correctly. Since the session of the user can expire before he finishes his work, or he can close his browser by accident, I'm storing this object using local storage, so I can change the page accordingly to what he has done should anything happen. Here are my functions:
function setObj(id, supervisor) {
localStorage.setItem(id, JSON.stringify(supervisor));
}
function getObj(key) {
var supervisor = JSON.parse(localStorage.getItem(key));
return supervisor;
}
So, I'm trying to add to the record whenever an user clicks in a checkbox. And this is where the problem happens. Here's the function:
function checkboxClicked(idCbx) {
var idSection = $("#ContentPlaceHolder1_hdnActualField").val();
var supervisor = getObj(idSection);
console.log(typeof (supervisor)); //Returns object, everythings fine
console.log(typeof (supervisor.change)); //Returns boolean
supervisor.change = true;
var idCheck = idCbx.split("_")[2]; //I just want a part of the name
console.log(typeof(supervisor.selections)); //Prints object
console.log("Length " + supervisor.selections.size); //Undefined!
supervisor.selections.set(idCheck, true); //Error! Note: The true is just for testing purposes
setObj(idSection, supervisor);
}
What am I doing wrong? Thanks!
Please look at this example, I removed the jquery id discovery for clarity. You'll need to adapt this to meet your needs but it should get you mostly there.
const mapToJSON = (map) => [...map];
const mapFromJSON = (json) => new Map(json);
function Supervisor(id) {
this.id = id;
this.verif = null;
this.selections = new Map();
this.change = false;
this.selections.set('blah', 'hello');
}
Supervisor.from = function (data) {
const id = data.id;
const supervisor = new Supervisor(id);
supervisor.verif = data.verif;
supervisor.selections = new Map(data.selections);
return supervisor;
};
Supervisor.prototype.toJSON = function() {
return {
id: this.id,
verif: this.verif,
selections: mapToJSON(this.selections)
}
}
const expected = new Supervisor(1);
console.log(expected);
const json = JSON.stringify(expected);
const actual = Supervisor.from(JSON.parse(json));
console.log(actual);
If you cant use the spread operation in 'mapToJSON' you could loop and push.
const mapToJSON = (map) => {
const result = [];
for (let entry of map.entries()) {
result.push(entry);
}
return result;
}
Really the only thing id change is have the constructor do less, just accept values, assign with minimal fiddling, and have a factory query the dom and populate the constructor with values. Maybe something like fromDOM() or something. This will make Supervisor more flexible and easier to test.
function Supervisor(options) {
this.id = options.id;
this.verif = null;
this.selections = options.selections || new Map();
this.change = false;
}
Supervisor.fromDOM = function(id) {
const selections = new Map();
const children = $("#ContentPlaceHolder1_checkboxes_div_" + id).children();
for (var i = 0; i < children.length; i++) {
if (i % 2 == 0) {
var checkbox = children[i];
var idCheck = checkbox.id.split("_")[2];
selections.set(idCheck, false);
}
}
return new Supervisor({ id: id, selections: selections });
};
console.log(Supervisor.fromDOM(2));
You can keep going and have another method that tries to parse a Supervisor from localStorageand default to the dom based factory if the localStorage one returns null.

Get one element from JS array, in any click

I have a constuctor function and a button. I want to get a name of one client in each click. but when I click one time, I get the name of all the clients in a sequence.
function Client(Name, Feedback) {
this.clientName = Name;
this.clientFeedback = Feedback;
}
let clients = [
new Client('Jo', 'hi'),
new Client('Mark', 'bye'),
]
let btnRight = document.getElementById('btnRight');
btnRight.addEventListener('click', () => {
for (let Client of clients) {
console.log(`${Client.clientName} says ${Client.clientFeedback}!`)
}
})
<button class="btn" id="btnRight">button</button>
I'm absolute beginner, so any feedback will help me
Don't loop in your event handler. Instead, keep track of where you are in the array outside the handler, for instance (see *** comments):
function Client(Name, Feedback){
this.clientName = Name;
this.clientFeedback = Feedback;
}
let clients = [
new Client('Jo', 'Hi' ),
new Client('Mark', 'Bye'),
];
let index = 0; // The next client to show
let btnRight = document.getElementById('btnRight');
btnRight.addEventListener('click', () => {
// *** Get this client
const client = clients[index];
// *** Set up the next click (looping around back to the start if necessary)
index = (index + 1) % clients.length;
// *** Show result
alert(`${client.clientName} says ${client.clientFeedback}!`)
});
<button class="btn" id="btnRight">button</button>
If you don't want to loop back to the beginning, keep a reference to the event handler and remove it when you run out of clients (or similar).
Some side notes:
I suggest learning the rules for where semicolons go and then consistently including them (my preference) or leaving them out (relying on automatic semicolon insertion), but not mixing the two. :-) My guess is that you just left some out accidentally — and fair enough, you're new!
I strongly, strongly recommend not putting the closing } at the end of the last statement in a block. It's hard to read (subjective) and hard to maintain (objective; you have to muck about if you need to add another statement). Use any of the standard styles, all of which put the closing } on its own line after the block.
I suggest not using initially-capped variables (client in your for (const Client of clients)) for things other than constructor functions (and type names, in TypeScript), at least not in code you'll be working on with other people or asking for help with, etc. The overwhelming convention is to start a variable with a lower case letter when it's not referring to a constructor function.
Finally, consistent indentation is useful for when you're reading code. I'm a strongly believer in four-space (or one tab) indentation, but two spaces is (sadly) very common. Whatever you choose, consistency is the key thing.
Suggest removing the foreach inside the click function. And add a variable to track the count of clicks.
function Client(Name, Feedback){
this.clientName = Name;
this.clientFeedback = Feedback;
}
let clients = [
new Client('Jo', 'Hi' ),
new Client('Mark', 'Bye'),
]
let btnRight = document.getElementById('btnRight');
let clickIndex = 0;
btnRight.addEventListener('click', () => {
alert(`${clients[clickIndex].clientName} says ${clients[clickIndex].clientFeedback}!`)
clickIndex += 1;
if(clickIndex > clients.length - 1)
clickIndex = 0;
})
function Client(Name, Feedback) {
this.clientName = Name;
this.clientFeedback = Feedback;
}
let clients = [
new Client('Jo', 'Hi'),
new Client('Mark', 'Bye'),
]
let currentIndex=0
let btnRight = document.getElementById('btnRight');
btnRight.addEventListener('click', () => {
alert(`${clients[currentIndex].clientName} says ${clients[currentIndex].clientFeedback}!`)
currentIndex++
if(currentIndex >= clients.length){
currentIndex=0;
}
})
<button class="btn" id="btnRight">button</button>
All of the above answers are correct, yet here is another solution using javascript yield:
function Client(Name, Feedback) {
this.clientName = Name;
this.clientFeedback = Feedback;
}
let clients = [
new Client('Jo', 'Hi'),
new Client('Mark', 'Bye'),
]
let btnRight = document.getElementById('btnRight');
function* yieldClient(index = 0) {
while(true) {
yield(clients[index]);
index = (index + 1) % 2;
}
}
const clientIterator = yieldClient();
btnRight.addEventListener('click', () => {
const Client = clientIterator.next().value;
console.log(`${Client.clientName} says ${Client.clientFeedback}!`)
})
<button class="btn" id="btnRight">button</button>

ForEach only looping through first item on DataSnapshot

I´m trying to loop through the content of a DataSnapshot and then depending on a condition do some work FOR EACH one of the elements but currently, the ForEach is only doing the work in the first item. The "serverStatus" sometimes is waiting and sometimes in "onCall". When the first item is "onCall" does not go through the rest of the items as I think is supposed to do. Below a snapchot of where I get the information from:
And here is my function:
exports.manageCallRequests = functions.database.ref('/resquests/{userId}').onCreate((snap, context) => {
const event = snap.val();
console.log("function manageCallRequests is being called")
var rootPath = admin.database().ref();
var userOnCall = context.params.userId;
var serversRef = rootPath.child('servers');
var callRequest = event;
var userTime = callRequest["time"];
var waiting= "waiting";
//We first get all the servers in ascending order depending on the last time they were used
var serversSorted = serversRef.orderByChild('lastTimeUsed')
//Gets the children on the "serversSorted" Query
return serversSorted.once("value").then(allServers =>{
//Checks if there is any child
if(allServers.hasChildren()){
allServers.forEach(async function(server) {
//we extract the value from the server variable, this contains all the information
//about each one of the servers we have
var serverInfo = server.val();
var serverKey = server.key;
var serverNumber = serverInfo["serverNumber"];
var serverStatus = serverInfo["serverStatus"];
console.log("server status "+serverStatus)
if(serverStatus === waiting){
const setCallRequest = await serversRef.child(serverKey).child("current").child("callRequest").set(callRequest);
const removeUserOnCall = await rootPath.child("resquests").child(userOnCall).remove();
const setServerStatus = await serversRef.child(serverKey).child("serverStatus").set("onCall");
}
});
}else{
console.log("No servers available")
}
});
});
I had the same behavior because my cloud function was exited before that all iterations were executed in the forEach loop.I get rid of it using this snippet of code:
for (const doc of querySnapshot.docs) {
// Do wathever you want
// for instance:
await doc.ref.update(newData);
}
I found 2 ways of getting this done. The first one is useful if we have a DataSnapshot without any OrderBy* call, in this case, would be:
var allServers = await serversRef.once("value");
for (let serverKey of Object.keys(allServers.val())){
var server = allServers[serverKey];
//Do some work
}
We need to first get the keys of the object to then be able to extract it from within the for loop, as explained here otherwise we´ll get a "TypeError: 'x' is not iterable"
Now the problem with this particular case is that a have a DataSnapshot that was previously sorted at var serversSorted = serversRef.orderByChild('lastTimeUsed') so when we call Object.keys(allServers.val()) the value returned is no longer sorted and that´s where forEach() comes in handy. It guarantees the children of a DataSnapshot will be iterated in their query order as explained here however for some reasons when doing some async work within the forEach loop this seems not to work, that´s why I had to do this:
var serversSorted = serversRef.orderByChild('lastTimeUsed')
var allServers = await serversSorted.once("value");
//Checks if there is any children
if (allServers.hasChildren()) {
//if there is iterate through the event that was passed in containing all
// the servers
var alreadyOnCall = false;
var arrayOfServers = []
var arrayOfKeys = []
allServers.forEach(function(individualServer){
arrayOfKeys.push(individualServer.key)
arrayOfServers.push(individualServer)
})
for (var serveIndex = 0; serveIndex < arrayOfServers.length;serveIndex++){
var serverObj = arrayOfServers[serveIndex]
var serverObject = serverObj.val()
var serverKey = arrayOfKeys[serveIndex]
var serverStatus = serverObject["serverStatus"];
var serverNumber = serverObject["serverNumber"];
console.log("server info "+serverStatus+" "+serverKey);
if (serverStatus === waiting && alreadyOnCall === false) {
const setCallRequest = await serversRef.child(serverKey).child("current").child("callRequest").set(callRequest);
const removeUserOnCall = await rootPath.child("resquests").child(userOnCall).remove();
const setServerStatus = await serversRef.child(serverKey).child("serverStatus").set("onCall");
alreadyOnCall= true
console.log("Call properly set");
}
}
}

Categories

Resources