Number not incrementing properly - javascript

I'm trying to learn Javascript by creating a Quiz App like it's mentioned here: Learn Javascript Properly I have trouble trying to do the more advanced quiz, I'm using Handlebars as my templating engine. I've stored the questions in an external JSON file, and the inputs are just a bunch of radio buttons.
index.html:
<!DOCTYPE html>
<html>
<head>
<title>Quiz Application</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div id="container">
<div id="quiz">
</div>
<button id="next-question">Next Question</button>
</div>
<!-- Templates -->
<script id="titleTemplate" type="text/x-handlebars-template">
<div id="quiz-question">{{ title }}</div>
</script>
<script id="choicesTemplate" type="text/x-handlebars-template">
<div id="choices">
<form id="choicesForm">
{{#each choices}}
<div class="choice">
<input type="radio" name="choices" id="choice{{#index}}" value="{{ #index }}">
<label for="choice{{ #index }}" value="{{ #index }}">{{ this }}</label>
</div>
{{/each}}
</form>
</div>
</script>
<script type="text/javascript" src="js/jquery-3.1.0.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.js"></script>
<script type="text/javascript" src="js/app.js"></script>
</body>
</html>
I've explained everything in the comments
app.js:
"use strict";
var Render = {
question: function(questions, currentIndex) {
var titleTemplate = Handlebars.compile($("#titleTemplate").html());
var choicesTemplate = Handlebars.compile($("#choicesTemplate").html());
var currentQuestion = questions[currentIndex];
Render.title(titleTemplate, currentQuestion);
Render.choices(choicesTemplate, currentQuestion);
},
title: function(titleTemplate, currentQuestion) {
$("#quiz").html(titleTemplate(currentQuestion));
},
choices: function(choicesTemplate, currentQuestion) {
$('#quiz-question').append(choicesTemplate(currentQuestion));
},
};
var Quiz = {
// Tracks the current questions
currentIndex: 0,
// The number of the correct answers
// I want to increment this when the user
// answers the correct question, for some reason it doesn't increment.
correctAnswers: 0,
// I first call this method to get the questions from JSON file
// since you can no longer have synchronous requests
getQuestions: function() {
$.getJSON('questions.json', Quiz.start);
},
// Starts the quiz
start: function(questions) {
// Check if the user is passed the last question
// in this case, if 6 > 5
if(Quiz.currentIndex + 1 > questions.length) {
Quiz.displayFinalScore(questions);
} else {
// Render the next question using the currentIndex
Render.question(questions, Quiz.currentIndex);
Quiz.handleQuestion(questions);
}
},
handleQuestion: function(questions) {
// Get's the correct answer from the JSON
var correctAnswer = questions[Quiz.currentIndex].correctAnswer;
// Check if the radio button is checked
if($("input[name=choices]:checked", "#choicesForm") == correctAnswer) {
// This doesn't increment like it suppose to
// It still stays at 0.
Quiz.correctAnswers += 1;
}
// Increment the index to change to the next question
Quiz.currentIndex += 1;
},
displayFinalScore: function(questions) {
// Simple console log
console.log("You have scored: " + Quiz.correctAnswers + " out of " + questions.length);
}
};
$(function() {
Quiz.getQuestions();
$("#next-question").on("click", Quiz.getQuestions);
})
As you can see I've explained in the comments, The problem is in incrementing the correctAnswers for some reason it doesn't get to the point where it compares both of the variables even if I choose the correct answer from the radio buttons.
questions.json:
[
{
"title":"When did the programming language C++ came out?",
"choices":[
1997,
1995,
2000,
1998
],
"correctAnswer":3
},
{
"title":"When Node.js came out?",
"choices":[
2010,
2011,
2009,
2006
],
"correctAnswer":2
},
{
"title":"What brand of laptop do I have?",
"choices":[
"HP",
"Acer",
"Dell",
"Lenovo"
],
"correctAnswer":0
},
{
"title":"How old am I?",
"choices":[
12,
20,
9,
16
],
"correctAnswer":3
},
{
"title":"How old is Google?",
"choices":[
12,
20,
18,
16
],
"correctAnswer":2
}
]

There are several issues with your code and they are:
In the start function you should change if condition expression to match array indexes, which in JavaScript are starting from 0 and the last element of array have index questions.length - 1.
In the start function you first render new question and then query DOM for selected element, which is already destroyed at this phase. So you need to first handleQuestion and only then render new one.
I also moved currentIndex increment code to the last step (after render)
Since handleQuestion should not be executed at the first render, I added this exotic Quiz.currentIndex && Quiz.handleQuestion(questions);, which actually means: "If currentIndex is 0, which in boolean expressions transforms to false, don't run the right part of boolean condition, which is Quiz.handleQuestion(questions)".
To fetch a numeric value of selected input, you should use parseInt($("input[name=choices]:checked", "#choicesForm").val(), 10)
The resulting code should look like this:
JavaScript
// Starts the quiz
start: function(questions) {
// Check if the user is passed the last question
// in this case, if 5 >= 5
if(Quiz.currentIndex >= questions.length) {
Quiz.displayFinalScore(questions);
} else {
Quiz.currentIndex && Quiz.handleQuestion(questions);
// Render the next question using the currentIndex
Render.question(questions, Quiz.currentIndex++);
}
},
handleQuestion: function(questions) {
// Get's the correct answer from the JSON
var correctAnswer = questions[Quiz.currentIndex-1].correctAnswer;
// Check if the radio button is checked
if(parseInt($("input[name=choices]:checked", "#choicesForm").val(), 10) === correctAnswer) {
// This doesn't increment like it suppose to
// It still stays at 0.
Quiz.correctAnswers += 1;
}
}
Live Demo
https://plnkr.co/edit/03IGvb6IZiw6QbziUpw6?p=preview

Related

How can I render objects to HTML with saving the previous value?

I have the following data structure, and I'm trying to render each object individually on click whiteout overwriting the previous value with the current value.
boardCollection =
[
{
id: 1,
dashboardType: "Simple",
fields: [
"Board naspa",
"Cea mai mare mica descriere"
]
},
{
id: 2,
dashboardType: "Simple",
fields: ["Titlu fara idei", "Descriere in speranta ca se va afisa"]
},
{
id: 3,
dashboardType: "Complex",
fields: ["Primu board complex", "descriere dorel", "Hai ca merge cu chiu cu vai"]
},
{
id: 4,
dashboardType: "Complex",
fields: ["Kaufland", " merge si asta ", "aaaaaaaaaaaaaaaaaaaaaaaaaa"]
}
]
in which I am accessing the elements in the following manner ->value/index are defined globally.
display() {
let currentElement = this.boardCollection[this.index]
this.value = currentElement;
if (this.index < this.boardCollection.length - 1) {
this.index++;
} else {
this.index = 0;
}
}
Here is the HTML and the way that i`m trying to render each object.
<div *ngIf="show">
<h1>{{value.dashboardType}}</h1>
<ol *ngFor="let prop of value.fields |keyvalue">
<li>{{prop.value }}</li>
</ol>
</div>
<button (click)="display()">Show</button>
show is set to true in the display method.
What I have achieved so far is to display each object or the properties from them, but each time the button is pressed, the current value will overwrite the previous value, therefore I'm looking for some help into saving the previous value in order to display each object so in the end to have all the objects from the Array rendered to the UI
I would have another array in the TypeScript and keep adding to this array as display is clicked.
boards = [];
...
display(index: number) {
let currentElement = this.boardCollection[this.index]
this.value = currentElement; // might not be needed
this.boards = [...this.boards, ...currentElement]; // append to this.boards immutably so change detection takes effect (this.boards.push won't force change detection)
if (this.index < this.boardCollection.length - 1) {
this.index++;
} else {
this.index = 0;
}
}
...
<div *ngFor="let board of boards>"
<h1>{{board.dashboardType}}</h1>
<ol *ngFor="let prop of board.fields |keyvalue">
<li>{{prop.value }}</li>
</ol>
</div>
<button (click)="display()">Show</button>
Clicking on Show each time should keep on displaying each one one by one.

Vue.js 2 - Array change detection

Here's a simplified version of my code :
<template>
/* ----------------------------------------------------------
* Displays a list of templates, #click, select the template
/* ----------------------------------------------------------
<ul>
<li
v-for="form in forms.forms"
#click="selectTemplate(form)"
:key="form.id"
:class="{selected: templateSelected == form}">
<h4>{{ form.name }}</h4>
<p>{{ form.description }}</p>
</li>
</ul>
/* --------------------------------------------------------
* Displays the "Editable fields" of the selected template
/* --------------------------------------------------------
<div class="form-group" v-for="(editableField, index) in editableFields" :key="editableField.id">
<input
type="text"
class="appfield appfield-block data-to-document"
:id="'item_'+index"
:name="editableField.tag"
v-model="editableField.value">
</div>
</template>
<script>
export default {
data: function () {
return {
editableFields: [],
}
},
methods: {
selectTemplate: function (form) {
/* ------------------
* My problem is here
*/ ------------------
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
}
}
}
</script>
Basically I want to update the array EditableFields each time the user clicks on a template. My problem is that Vuejs does not update the display because the detection is not triggered. I've read the documentation here which advise to either $set the array or use Array instance methods only such as splice and push.
The code above (with push) works but the array is never emptied and therefore, "editable fields" keep pilling up, which is not a behavior I desire.
In order to empty the array before filling it again with fresh data, I tried several things with no luck :
this.editableFields.splice(0, this.editableFields.length);
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
==> Does not update the display
for (let i = 0; i < form.editable_fields.length; i++) {
this.$set(this.editableFields, i, form.editable_fields[i]);
}
==> Does not update the display
this.editableFields = form.editable_fields;
==> Does not update the display
Something I haven't tried yet is setting a whole new array with the fresh data but I can't understand how I can put that in place since I want the user to be able to click (and change the template selection) more than once.
I banged my head on that problem for a few hours now, I'd appreciate any help.
Thank you in advance :) !
I've got no problem using splice + push. The reactivity should be triggered normally as described in the link you provided.
See my code sample:
new Vue({
el: '#app',
data: function() {
return {
forms: {
forms: [{
id: 'form1',
editable_fields: [{
id: 'form1_field1',
value: 'form1_field1_value'
},
{
id: 'form1_field2',
value: 'form1_field2_value'
}
]
},
{
id: 'form2',
editable_fields: [{
id: 'form2_field1',
value: 'form2_field1_value'
},
{
id: 'form2_field2',
value: 'form2_field2_value'
}
]
}
]
},
editableFields: []
}
},
methods: {
selectTemplate(form) {
this.editableFields.splice(0, this.editableFields.length);
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
<ul>
<li v-for="form in forms.forms"
#click="selectTemplate(form)"
:key="form.id">
<h4>{{ form.id }}</h4>
</li>
</ul>
<div class="form-group"
v-for="(editableField, index) in editableFields"
:key="editableField.id">
{{ editableField.id }}:
<input type="text" v-model="editableField.value">
</div>
</div>
Problem solved... Another remote part of the code was in fact, causing the problem.
For future reference, this solution is the correct one :
this.editableFields.splice(0, this.editableFields.length);
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
Using only Array instance methods is the way to go with Vuejs.

Javascript Object Assignment gottcha

I came across this whilst building sort buttons for a data table. I wanted to have each sort button to toggle between a DESC (sort down) and ASC (sort up) and inactive - the default sort.
So I created two models one was going to be the one that changed as the program progressed and one was a default to reset the values
var model = {
order: {
field: 'creationDate',
direction: 'DESC'
},
orderDefault: {
field: 'creationDate',
direction: 'DESC'
}
}
$('button').click(function() {
if (model.order.field == "creationDate") {
model.order.field = 'newField'
} else if (model.order.direction == 'DESC') {
model.order.direction = 'ASC'
} else {
// This casuses the orderDefault object to be a clone of the order object
// so assignment of values in model.order are refelected in model.orderDefault GOTCHA!!!!
model.order = model.orderDefault
}
$('#result').html('field:' + model.order.field + " direction:" + model.order.direction);
}
)
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<button class="btn btn-primary">
click me
</button>
<div id="result">
</div>
So you will see that it all works as expected until the point where we reset the order object. the second time we get to this point in the code you will see that it never resets. It seems that the orderDefault obj becomes an alias of order obj.
the answer is that instead of assigning orderDefault you need to assign the values of orderDefault
so NOT
model.order = model.orderDefault
BUT
model.order.direction = model.orderDefault.direction
model.order.field = model.orderDefault.field
I hope this helps someone.

Reference Global jquery function variable within foreach loop

I want to change the value of a global jquery variable within the foreach loop every time there is a new model item. I want to add new dates to the calendar but cant do that until I can access these functions from foreach loop.
*edit***
I over simplified my example for the question which was answered correctly by vov v. The actual code will do a lot more than add a value as it will add data to a calendar. I've added more code to show what it will do a little better
jquery:
<div id="calendar" style="width: 500px;" />
<script type="text/javascript">
$(document).ready(function () {
var calendar = $('#calendar').glDatePicker(
{
showAlways: true,
borderSize: 0,
dowOffset: 1,
selectableDOW: [1, 2, 3, 4, 5],
selectableYears: [2012, 2013, 2014, 2015],
specialDates: [
{
date: new Date(2013, 0, 8),
data: { message: 'Meeting every day 8 of the month' },
repeatMonth: true
},
{
date: new Date(2013, 5, 7),
data: { message: 'Meeting every day 8 of the month' }
},
],
onClick: function (target, cell, date, data) {
target.val(date.getFullYear() + ' - ' +
date.getMonth() + ' - ' +
date.getDate());
if (data != null) {
alert(data.message + '\n' + date);
}
}
}).glDatePicker(true);
$('#visible').change(function () {
var showAlways = eval($(this).val());
calendar.options.showAlways = showAlways;
if (!showAlways) {
calendar.hide();
}
else {
calendar.show();
}
});
});
var value = 0;
$('#total').click(function () {
alert(value);
});
function add() {
// will eventually add new specialDates to the calendar taken from model items
//test lines
//value = value + 1;
//return value;
}
</script>
razor view:
<input type="button" id="total" />
#foreach (var item in Model){
if (item.AppointmentStatus == "active")
{
// item display code
#: <script type="text/javascript"> add();</script>
}
if (item.AppointmentStatus == "history")
{
// item display code
}
}
I run this and get error below as it doesnt see the other code
'0x800a1391 - JavaScript runtime error: 'add' is undefined'
If you just want to capture "a count" that you want to send down to your client then you can simply do it like this:
<script>
var value = '#Model.Count';
// the rest of you script goes here
$(document).ready(function () {
$('#total').click(function () {
alert(value);
}
});
</script>
So say you have 7 items in your model, then the html that will be generated is this:
var value = 7;
and when you click on that total element it will give you an alert with the text 7.
Try moving your add function and variable declaration outside of doc ready. The functions in doc ready are not available until your razor has executed.

Meteor.js: Client doesn't subscribe to collection

I created a little example for myself to test some stuff with Meteor. But right now it looks like I can't subscribe to a collection, I published on the server side. I hope somebody can tell me where the bug is.
server/model.js
Test = new Meteor.Collection("test");
if (Test.find().count() < 1) {
Test.insert({id: 1,
name: "test1"});
Test.insert({id: 2,
name: "test2"});
}
Meteor.publish('test', function () {
return Test.find();
});
client/test.js
Meteor.subscribe("test");
Test = new Meteor.Collection("test");
Template.hello.test = function () {
console.log(Test.find().count());//returns 0
return Test.findOne();
}
Template.hello.events = {
'click input' : function () {
// template data, if any, is available in 'this'
if (typeof console !== 'undefined')
console.log("You pressed the button");
}
};
client/test.html
<head>
<title>test</title>
</head>
<body>
{{> hello}}
</body>
<template name="hello">
<h1>Hello World!</h1>
{{#with test}}
ID: {{id}} Name: {{name}}
{{/with}}
<input type="button" value="Click" />
</template>
EDIT 1
I want to change the object test, findOne() returns. Let's say for adding an attribute avg which contains the average value of two numbers (test.number1 and test.number2). In my opinion this should look like the following code. But javascript is not synchronous, so this won't work.
Template.hello.test = function () {
var test = Test.findOne();
test.avg = (test.number1 + test.number2) / 2;
return test;
}
EDIT 2
This code worked for me. Now I have to rethink why this solution with 'if (test)' just works with findOne() without a selector in my original project.
Template.hello.test = function () {
var avg = 0, total = 0, cursor = Test.find(), count = cursor.count();
cursor.forEach(function(e)
{
total += e.number;
});
avg = total / count;
var test = Test.findOne({id: 1});
if (test) {
test.avg = avg;
}
return test;
}
The latency the client db uses to replicate data might cause the situation wherein the cursor reckons no results. This especially occurs when the template is immediately rendered as the app loads.
One workaround is to observe query documents as they enter the result set. Hence, something like the following for example happens to work pretty well:
Meteor.subscribe("Coll");
var cursor = Coll.find();
cursor.observe({
"added": function (doc) {
... something...
}
})
Try to surround {{#with test}}...{{/with}} with {{#if}}...{{/if}} statement (because in first data push test does not have id and name fields):
<head>
<title>test</title>
</head>
<body>
{{> hello}}
</body>
<template name="hello">
<h1>Hello World!</h1>
{{#if test}}
{{#with test}}
ID: {{id}} Name: {{name}}
{{/with}}
{{/if}}
<input type="button" value="Click" />
</template>
As a result:
UPDATE:
This code performs calculation of average of field number in all records:
model.js:
Test = new Meteor.Collection("test");
Test.remove({});
if (Test.find().count() < 1)
{
Test.insert({id: 1,
name: "test1",
number: 13});
Test.insert({id: 2,
name: "test2",
number: 75});
}
test.js
Test = new Meteor.Collection("test");
Template.hello.test = function () {
var avg = 0, total = 0, cursor = Test.find(), count = cursor.count();
cursor.forEach(function(e)
{
total += e.number;
});
avg = total / count;
return { "obj": Test.findOne(), "avg": avg };
}
UPDATE 2:
This code snippet works for me:
var test = Test.findOne();
if (test)
{
test.rnd = Math.random();
}
return test;
Maybe you should try to wrap assignment code into if statement too?

Categories