I am trying to build a simple quiz app with javascript, and it goes all well, but when it comes to validation something weird happens, it validates my code twice! Once with the correct answer of previous question and another time with the correct one for the current one!
Can anyone please check my code out? Finds where the bug is? Thanks so much
(function () {
// Question Function Constructor
const Question = function (question, answers, correct) {
this.question = question;
this.answers = answers;
this.correct = correct;
}
// Questions Prototypes
Question.prototype.displayQuestion = function () {
// getting html elements
const qHolder = $('#question').get(0);
const list = $('#listOfAnswers').get(0);
// show questions
$(qHolder).empty();
$(qHolder).append(this.question);
// show answers
this.answers.forEach(answer => {
let li = ` <li>
<label for="${answer}">
<input class="option" type="radio" name="answer" value="${answer}" id="${answer}"> ${answer}
</label>
</li>`;
$(list).append(li);
});
}
Question.prototype.validate = function (ans, callback) {
let score;
if (ans === this.correct) {
console.log('**** TRUE ****');
score = callback(true);
console.log(`YOURE ANSWERE === ${ans}`);
} else {
console.log('---- FALSE ----');
score = callback(false);
console.log(`YOURE ANSWERE === ${ans}`);
console.log(`CORRECT ANSWER IS =====> ${this.correct}`);
}
this.displayScore(score);
}
Question.prototype.displayScore = function (score) {
console.log(`your current score is : ${score}`);
console.log(`=======================================================`);
}
// instance of Questions
const q1 = new Question('what\'s max\'s name?', [
'lax',
'max',
'wax',
'pax',
], 1);
const q2 = new Question('2+2?', [
'4',
'12',
'99',
'none',
], 0);
const q3 = new Question('... jobs?!', [
'andrew',
'what?!',
'why?!',
'steve',
], 3);
const q4 = new Question('which one is not a programming language?', [
'javascript',
'python',
'apple',
'c#',
], 2);
// Array of Questions
const Questions = [q1, q2, q3, q4];
// console.log(Questions);
function score() {
let sc = 0;
return function (correct) {
if (correct) {
sc++;
}
return sc;
}
}
let keepScore = score();
function nextQuestion() {
// getting the list and emptying the it for new list items
const list = $('#listOfAnswers');
$(list).empty();
// alerting in the console that its a new question
console.log('A NEW QUESTION');
//generate a random number
let n = Math.floor(Math.random() * Questions.length);
console.log(`random number generated --> ${n}`);
// display the question
Questions[n].displayQuestion();
console.log(`answer to the question ==>${Questions[n].correct}`);
// validating the answer by click on the submit button
const submitBtn = $('#submitBtn');
submitBtn.on('click', function (e) {
e.preventDefault();
let options = $('.option');
for (i = 0; i < options.length; i++) {
const option = options[i];
if ($(option).is(':checked')) {
// console.log(option);
userAnswerIndex = i;
// validation
Questions[n].validate(userAnswerIndex, keepScore);
}
}
})
}
// displaying next question by clicking next
const nextQbtn = document.querySelector('#next');
nextQbtn.addEventListener('click', nextQuestion);
nextQuestion();
})();
here is my HTML Markup, thanks again --->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="/style.css">
<title>Quiz App</title>
</head>
<body dir="rtl">
<header>
<h3>welcome to the quiz app</h3>
</header>
<div class="container">
<div id="card">
<header>
<p id="question">
<!-- Question displays here -->
</p>
</header>
<form id="answers">
<ol id="listOfAnswers">
<!-- answers will generate here -->
</ol>
</form>
<button type="submit" id="submitBtn">submit</button>
<button id="next">next!</button>
</div>
</div>
<script src="http://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="/quiz.js"></script>
</body>
</html>
I was not able to run the code to be sure since I didn't have the relevant markup but from reading it seems like problem can be in this block of code
submitBtn.on('click', function (e) {
e.preventDefault();
let options = $('.option');
for (i = 0; i < options.length; i++) {
const option = options[i];
if ($(option).is(':checked')) {
// console.log(option);
userAnswerIndex = i;
// validation
Questions[n].validate(userAnswerIndex, keepScore);
}
}
})
Let's go to third line which is let options = $('.option'); now again, since I don't have markup I can be wrong but this seems a bit fishy. You are selecting all the elements with class option and then iterating over them. In each iteration you check if option is checked, if it is you validate it.
Now in only way in which you can confirm that validate will run only once is when $('.option') contains only one selected element. But if you have multiple selects or input elements with class option and user has selected multiple of them you might get more than one selected elements. In that case you might see question being validated more than one time
Your problem is that every time the user presses the next button a new click event handler is added to the submit button.
You would think that since it is the same function that wouldn't happen but you are actually doing it again and again. If the user clicks three times the next button before he submits it will trigger the validation functionality three times and so on...
You can handle this by either removing all other click event handlers every time you add your event. (The simplest one)
You can do that with the jquery off like this
submitBtn.off().on('click', function (e) {
Where you just unbind all click event handlers before you add your new one. (This way you will always have one click event handler on your submit button). Of course If you want to add other click event handlers to this button (not very often but maybe sometimes you want to do that) you should use it with caution
Another way to go is just not add the click event handler every time the next button is pressed. Just add the event once outside of your NextQuestion function like this (outside from document ready if you want or inside it doesn't matter)
$(document).on("click", "#submitBtn", function(e) {
e.preventDefault();
var options = $('.option');
for (i = 0; i < options.length; i++) {
var option = options[i];
if ($(option).is(':checked')) {
// console.log(option);
userAnswerIndex = i;
// validation
Questions[n].validate(userAnswerIndex, keepScore);
}
}
});
or
function nextQuestion(){
// do everything else
}
var submitBtn = $('#submitBtn');
submitBtn.on('click', function (e) {
e.preventDefault();
var options = $('.option');
for (i = 0; i < options.length; i++) {
var option = options[i];
if ($(option).is(':checked')) {
// console.log(option);
userAnswerIndex = i;
// validation
Questions[n].validate(userAnswerIndex, keepScore);
}
}
})
Related
I have a quiz app application that I am working on that dynamically creates 2-4 buttons for the answer. However, if you click on an answer, you can keep clicking on the same answer or keep clicking the other answers. I want the user to be able to click one of the buttons but then not be able to keep clicking. caveat though: when a user clicks one of the buttons, a new "Next" button gets created and that one does still need it's click event.
tl;dr
I need dynamically created buttons to be clickable only once but a "Next" button to still be clickable.
Code:
function renderButtons() {
var answerContainer = document.getElementById("answer-buttons");
answerContainer.innerHTML = "";
for (var i = 0; i < 4; i++) {
var button = document.createElement("button");
button.classList.add("btn");
button.setAttribute("id", "answerBtns");
button.hasAttribute("data-correct");
button.setAttribute("data-correct", questions[count].answers[i].correct);
button.onclick = btnclick;
button.textContent = questions[count].answers[i].text;
answerContainer.appendChild(button);
}
}
// if user clicks on button check if true
function btnclick(e) {
e.preventDefault();
var value = event.target.dataset.correct;
scoreBox.textContent = "Score: " + score;
if (value === "true") {
score += 5;
document.body.style.background = "green";
} else {
document.body.style.background = "red";
}
var next = document.getElementById("answer-buttons");
var nextBtn = document.createElement("button");
nextBtn.classList.add("nextBtn");
nextBtn.textContent = "Next";
next.appendChild(nextBtn);
nextBtn.onclick = nextBtnFx;
if you want to see what I'm talking about, the app can be found here:
https://andrethetallguy.github.io/Animal-Quiz/
Thanks!!!
In the handler function nextBtnFx you could disable the button with
this.disabled = "true"
that would make it unclickable after the first click
ref: https://stackoverflow.com/a/17115132/13998159
<!DOCTYPE html>
<html>
<head>
<title>Button</title>
</head>
<body>
<input type="button" id="btn01" value="OK">
<button onclick="disableElement()">Disable</button>
<button onclick="enableElement()">Enable</button>
<script>
function disableElement() {
document.getElementById("btn01").disabled = true;
}
function enableElement() {
document.getElementById("btn01").disabled = false;
}
</script>
</body>
</html>
You can use these functions to disable/enable the buttons.
I am trying to create a site where objects can be added to the list an then delete by the button.For now programm can add to the list and has some other functions,but I can't create a "delete" function to the buttons.I pretty much understand why:the function I created for the list(line-through and text color when clicked) is responding when I click on the appended button,because it thinks it belongs to the list.In particular,error is:
script.js:55 Uncaught TypeError: Cannot read property 'classList' of undefined
at toggle (script.js:55)
at HTMLUListElement.whenClicked (script.js:65)
But problem is,the only solution to the problem I can find is rewriting the whole program and also making it much more complex:I can create an array of the li objects that were written in HTML and then append them through while loop to the ul and in the same loop append the buttons,so they won't be one object.That's why I am asking you all for help.Can someone advise me how to fix situation withour making everything so complex in the end?Thanks in advance.
CODE
Note:I created one duplicate of addButton function to see if I can just use buttons at the end of ul,but it's too much user unfriendly,that's why it is in the comments.Also there is small deleteLi function,that is suppoused to be acessed in the addButton function so that button can delete li.
var button = document.getElementById("button");
var modify_list = document.getElementById("userinput");
var ul = document.querySelector("ul");
var li = document.querySelectorAll("li");
var i = 0; //Stored value so we can acces to it later in the function
while (li.length > i) {
li[i].classList.add(i);
li[i].classList.add('done');
li[i].classList.add('cursor');
i++
}
var n = 0
while (li.length > n){
li[n].classList.toggle("done");
n++
}
function inputLength(){
return modify_list.value.length;
}
function addToTheList(){
var li = document.createElement("li");
li.appendChild(document.createTextNode(modify_list.value));
ul.appendChild(li);
// createNewButton();
modify_list.value = '';
}
function addAfterClick(){
if (inputLength() === 0){
alert("Please,don\'t enter the empty list");
}else {
addToTheList();
}
}
function addAfterEnter(key){
if (key.keyCode === 13 && inputLength() > 0){
addToTheList();
}
}
button.addEventListener("click", addAfterClick);
modify_list.addEventListener("keypress", addAfterEnter);
function toggle(number){
li[number].classList.toggle("done");
}
ul.addEventListener("click", whenClicked);
function whenClicked(event){
var li_number = event.target.className[0];
//In JS it doesn't matter in some occasions if it's a string or number,I suppouse.
// var li_number = Number(li_number_string);
// console.log(li_number);
toggle(li_number);
}
// Create buttons and their functions
function addButton(list_number) {
var localButton = document.createElement("button");
var br = document.createElement("br");
localButton.innerText = "Delete";
localButton.id = "myButton" + list_number;
li[list_number].appendChild(br);
li[list_number].appendChild(localButton);
localButton.onclick = deleteLi(list_number);
}
// function addButton() {
// var localButton = document.createElement("button");
// localButton.innerText = "Delete";
// localButton.id = "myButton" + list_number;
// ul.appendChild(localButton);
// // localButton.onclick = deleteLi(list_number);
// }
var list_number = 0
// creates buttons for the li that were already created
while (li.length > list_number){
addButton(list_number);
list_number++;
}
// creates buttons for new li items.
function createNewButton(){
var number = window['list_number'];
addButton(number);
window['list_number'] = number + 1;
}
function deleteLi(list_number){
// var list_element = document.getElementById('myButton' + list_number)
ul.removeChild(ul.childNodes[list_number])
}
.done {
color: red;
text-decoration: line-through;
}
.cursor{
cursor: pointer;
}
<!DOCTYPE html>
<html>
<head>
<title>DOM</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<h1>What plans do I have till the end of the summer?</h1>
<p>They are:</p>
<input type="text" name="add activities" id ="userinput" placeholder="add activities">
<button id="button">Send</button>
<ul>
<li>Learn German</li>
<li>Learn Japanese</li>
<li>Learn Java Script</li>
<li>Physical activities</li>
</ul>
<script type="text/javascript" src="script.js"></script>
</body>
</html>
In addbutton function in the end you should do :
localButton.onclick = () => deleteLi(list_number);
So you have to pass a function to onclik instead of the result of deleteLi.
I want to make my own grocery/task list in JavaScript and HTML. I can add things to the list no problem, but I have two problems:
If I added items to the list, then press a "clear" button to erase everything in the list, it works. However, when I add things back into the list after clearing it, my console notifies me that my array is still empty, despite new tasks being shown on the screen.
My second problem is bigger. See, all my tasks are in what I thought was an array, but are actually an HTML collection. So, when I try to set an onclick event, it just won't run. I have no idea why and I tried using the following question: Removing HTMLCollection elements from the DOM
. It didn't work. I tried using item, HTMLCollection item() Method. Didn't work. My final attempt was using for(let thing of... but still no results.
Here is my code:
let listGro = document.getElementById("list");
let aButton = document.getElementById("add");
let cButton = document.getElementById("clear");
let tasks = document.getElementsByTagName("li");
aButton.onclick = function addItem() {
let newThing = prompt("What do you want to add?"); // asking what the person wants
if (newThing) { // checking if the user actually added something
let newItemList = document.createElement("li"); //create item list
newItemList.className = "item";
let newItemListText = document.createTextNode(newThing); // create text
newItemList.appendChild(newItemListText); // append text
listGro.appendChild(newItemList); // append new element
console.log(`New task added\nNumber of tasks in the list: ${tasks.length}`);
} else {
alert("You can't add nothing");
}
};
cButton.onclick = function clearList() {
var conf = confirm("Are you sure you want to clear the list?");
if (conf && tasks.length != 0) {
for (let i = 0; i < tasks.length; i++) {
tasks[i].style.display = "none";
}
tasks = [];
}
}
for(let thing of tasks) {
//tasks[i].onclick = function removeOrEditItem() {
/*let demand = prompt("Do you want to edit or remove the item?\nPlease answer with 'edit' or 'remove'\nIf this is a mistake, just enter nothing.");
if (demand === "edit") {
let editItem = prompt("What is your new thing?");
tasks[i].innerHTML = editItem;
} else if (demand === "remove") {
tasks[i].splice(i, 1);
}
console.log("clicked");
};*/
// The thing above was a previous attempt with for(let i; i< items.length... you can work with that or below.
thing.onclick = function removeTask() {
thing.style.display = "none";
console.log("removed");
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Website template</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css"/>
</head>
<body>
<h1>Grocery lists manager</h1>
<h2>Welcome! here you can manage your grocery list!</h2>
<ul id = "list"><span>List:</span></ul>
<button id = "add">Add</button>
<button id = "clear">Clear list</button>
</body>
<script src = "script.js"></script>
</html>
The list of tasks remains empty when you clear it, because you're overwriting an HTML collection with an empty array. You assigned document.getElementsByTagName("li"); to tasks, which is a live collection. Reassigning [] to it will give it a non-live empty array. Instead of visually hiding the tasks with tasks[i].style.display="none and manually resetting the tasks collection, you should remove each task element from the DOM and let your live collection update itself automatically.
Removing a collection from the DOM has some pitfalls, but would typically be accomplished with something like the following:
while(tasks.length > 0) {
tasks[0].parentNode.removeChild(tasks[0]);
}
Your event listeners are not being added because the code that adds them only runs once, when the script is loaded. And of course at the instance when the script is loaded, there are no tasks in the list. Add the event listener to the individual newItemList when it's first added.
Make sure that you remove the task from the DOM and don't just hide it.
newItemList.onclick = function removeTask() {
newItemList.parentNode.removeChild(newItemList);
console.log("removed");
}
Your complete JavaScript might look like:
let listGro = document.getElementById("list");
let aButton = document.getElementById("add");
let cButton = document.getElementById("clear");
let tasks = document.getElementsByTagName("li");
aButton.onclick = function addItem() {
let newThing = prompt("What do you want to add?"); // asking what the person wants
if (newThing) { // checking if the user actually added something
let newItemList = document.createElement("li"); //create item list
newItemList.className = "item";
let newItemListText = document.createTextNode(newThing); // create text
newItemList.appendChild(newItemListText); // append text
newItemList.onclick = function removeTask() {
newItemList.parentNode.removeChild(newItemList);
console.log("removed");
}
listGro.appendChild(newItemList); // append new element
console.log(`New task added\nNumber of tasks in the list: ${tasks.length}`);
} else {
alert("You can't add nothing");
}
};
cButton.onclick = function clearList() {
var conf = confirm("Are you sure you want to clear the list?");
if (conf && tasks.length != 0) {
while(tasks.length > 0) {
tasks[0].parentNode.removeChild(tasks[0]);
}
}
};
I'm creating a basic to do list in Vanilla JS, I'm using Handlebars to keep the HTML & JS separate.
Everything was going fine till I came to the delete method. Because my delete button is inside my HTML and not created inside my JS I'm finding it hard to select and delete items from the array.
I thought I'd found a way around it by looping over them but the issue with this is it tries to grab the buttons on page load, and so it returns always an empty array as on page load there are no delete buttons as no to do has been added at that point.
I've also tried putting the delete method inside the add method to counter this but this also presented issues.
Simply, can someone give me an example of a working delete method that removes the relevant item from the array using splice.
Cheers
HTML
<input id="add-to-do-value" type="text" placeholder="Add to do">
<button id="add-to-do">Add</button>
<div id="to-do-app"></div>
<script type="text/javascript" src="js/handlebars.js"></script>
<script id="to-do-template" type="text/template">
<ul>
{{#this}}
<div>
<li id={{id}}>
{{value}}
<button class="delete-btn" id={{id}}>Delete</button>
</li>
</div>
{{/this}}
</ul>
</script>
<script type="text/javascript" src="js/app.js"></script>
JS
(function() {
// Data array to store to dos
var data = [];
// Cache dom
var toDoApp = document.getElementById('to-do-app');
var toDoTemplate = document.getElementById('to-do-template');
var addToDo = document.getElementById('add-to-do');
var addToDoValue = document.getElementById('add-to-do-value');
var toDoTemplate = Handlebars.compile(toDoTemplate.innerHTML);
// Render HTML
var render = function() {
toDoApp.innerHTML = toDoTemplate(data);
}
// Add to dos
var add = function() {
var toDoValue = addToDoValue.value;
if(toDoValue) {
var toDoObj = {
value: toDoValue,
id: Date.now(),
}
data.push(toDoObj);
}
render();
}
// Delete to dos
var deleteBtn = document.querySelectorAll('.delete-btn');
for(i=0; i<deleteBtn.length; i++) {
deleteBtn[i].addEventListener("click", function(){
for(j=0; j<data.length; j++) {
if(data[j].id == this.id) {
data.splice(data[j], 1);
render();
}
}
});
}
// Bind events
addToDo.addEventListener("click", add);
})();
The fact that you're using Handlebars makes the whole thing unnecessary complex. I would suggest that you don't use innerHTML, but other parts of the DOM API instead to be able to easily access the elements you need. For more complex todo items, I would consider using <template>s.
Anyway, you have to bind the event listener for removing the item when you create the new item (i.e. in the add function):
var todos = [];
var input = document.querySelector('input');
var addButton = document.querySelector('button');
var container = document.querySelector('ul');
var add = function () {
var content = input.value;
input.value = '';
var id = Date.now();
var li = document.createElement('li');
li.appendChild(document.createTextNode(content));
var button = document.createElement('button');
button.textContent = 'Delete';
button.addEventListener('click', remove.bind(null, id));
li.appendChild(button);
todos.push({ content, id, element: li });
container.appendChild(li);
};
var remove = function (id) {
var todo = todos.find(todo => todo.id === id);
container.removeChild(todo.element);
todos = todos.filter(t => t !== todo);
};
addButton.addEventListener('click', add);
<input type="text" placeholder="Add to do">
<button>Add</button>
<ul></ul>
Here's the code...
https://jsfiddle.net/6n2k65zs/
Try add a new item, you'll see its not working for some reason but it should be...
I can't spot any errors in the code, can someone help me out please?
And does anyone know any good debuggers? debugging JS is a nightmare!
Thanks.
<!DOCTYPE html>
<html>
<head>
<title>JavaScript To-Do List</title>
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<input id="input" type="text">
<button id="btn">Add</button>
<hr>
<ul id="todo">
</ul>
<ul id="done">
</ul>
<!-- javascript anonymous self-invoking function -->
<!-- Function expressions will execute automatically -->
<script>
// from outside the action you wont be able to access the variables
// prevents another variable with a same name from conflicting
(function(){
var input = document.getElementById('input');
var btn = document.getElementById('btn');
// Object for the lists
// the reason im using ID is because ID can only be named once rather than a class which can be named 100's of times
var lists = {
todo:document.getElementById('todo'),
done:document.getElementById('done')
};
/* Parameter is string
create a list element which is stored in 'el' and returns it
*/
var makeTaskHtml = function(str, onCheck) {
var el = document.createElement('li');
var checkbox = document.createElement('input');
var label = document.createElement('span');
label.textContent = str;
checkbox.type = 'checkbox';
checkbox.addEventListener('click', onCheck);
// el.textContent = str;
// can use this method to move an element from one element to another
el.appendChild(checkbox);
el.appendChild(label);
// Text content is grabbing the text from the text box and storing it in variable el.
return el;
};
var addTask = function(task) {
lists.todo.appendChild(task);
};
var onCheck = function(event){
var task = event.target.parentElement; //targets the item clicked
var list = task.parentElement.id;
//lists.done.appendChild(task);
//swaps the 2 objects around
lists[list === 'done' ? 'todo' : 'done'].appendChild(task);
this.checked = false;
input.focus();
};
var onInput = function() {
var str = input.value.trim; // trim removes white space...
if (str.length > 0) {
addTask(makeTaskHtml(str, onCheck));
input.value = '';
input.focus();
}
};
btn.addEventListener('click', onInput);
input.addEventListener('keyup', function(event){
var code = event.keyCode;
console.log(code);
if (code === 13) {
onInput();
}
});
input.focus();
addTask(lists.todo, makeTaskHtml('Test done', onCheck));
}());
</script>
</body>
</html>
It appears to me you are not calling trim as a method, but accessing it as a variable?
Try add the () in trim:
var onInput = function() {
var str = input.value.trim(); // trim removes white space...
Your addTask function is being called with 3 parameters:
addTask(lists.todo, makeTaskHtml('Test done', onCheck));
but the function definition for addTask only takes one parameter:
var addTask = function(task)
so you need to just call addTask with just makeTaskHtml parameter, and not lists.todo which is already referenced inside the addTask function or onCheck
Or for debugging in Chrome, try Cmd-Alt–I in (Mac) or Ctrl-Alt-I (Windows).
First of all, you shouldn't put your scripts inline in JSFiddle – put them in the JS box to protect everyone's sanity! It's what it's made for...
There are other issues in the code, but the main issue seems to be in this line:
var str = input.value.trim;
Here, you're assigning str to the JS function trim. You want to assign it the the results of trim(), so try:
var str = input.value.trim();
You're still getting other errors in the console, but the basics seem to work.