How do I calculate total value of multiple radio using ractive - javascript

I have multiple questions generated within ractive.js loop. Each question has multiple answers with different prices. I need to calculate total price and recalculate it every time selected answers change. Need help with this.
I made this codepen: http://codepen.io/Nitoiti/pen/GjxXvo
<h1>Get total price</h1>
<div id='container'></div>
<script id='template' type='text/ractive'>
{{#each designQuestions}}
<p><span>{{id}} </span> {{question}}</p>
{{#each answers}}
<label><input type='radio' name='designQuestions-answers-{{id}}' value='{{price}}' {{#if selected}} checked {{/if}} >{{answer}} - {{price}}</label>
{{/each}}
{{/each}}
<table class="total">
<tr>
<td>total price:</td>
<td>{{total}}</td> <!-- how to calculate total price and change it on radio buttons change? -->
</tr>
</table>
</script>
<script src='http://cdn.ractivejs.org/latest/ractive.min.js'></script>
<script>
var ractive, designQuestions;
designQuestions = [
{ id: '1',
question: 'Question1 ?',
answers: [
{answer:'answer1',price:'222', selected: 'selected'},
{answer:'answer2',price:'553'},
{answer:'answer3',price:'22'},
{answer:'answer4',price:'442'}
]
},
{ id: '2',
question: 'Question2 ?',
answers: [
{answer:'answer1',price:'22'},
{answer:'answer2',price:'55', selected: 'selected'},
{answer:'answer3',price:'0'},
{answer:'answer4',price:'44'}
]
}
];
var ractive = new Ractive({
// The `el` option can be a node, an ID, or a CSS selector.
el: '#container',
// We could pass in a string, but for the sake of convenience
// we're passing the ID of the <script> tag above.
template: '#template',
// Here, we're passing in some initial data
data: {
designQuestions: designQuestions
}
});
</script>

Well' i've found solution myself.
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<title>Ractive test</title>
</head>
<body>
<h1>Калькулятор</h1>
<!--
1. This is the element we'll render our Ractive to.
-->
<div id='container'></div>
<!--
2. You can load a template in many ways. For convenience, we'll include it in
a script tag so that we don't need to mess around with AJAX or multiline strings.
Note that we've set the type attribute to 'text/ractive' - though it can be
just about anything except 'text/javascript'
-->
<script id='template' type='text/ractive'>
{{#each designQuestions}}
<p><span>{{id}} </span> {{question}}</p>
{{#each answers}}
<label><input on-change='calc' type='radio' name='{{select}}' value='{{selected}}' >{{answer}} - {{price}}</label>
{{/each}}
{{/each}}
<table class="total">
<tr>
<td>Итого:</td>
<td>{{sum}}</td>
</tr>
</table>
</script>
<script src='http://cdn.ractivejs.org/latest/ractive.min.js'></script>
<script>
var ractive, designQuestions;
designQuestions = [
{ id: '1',
question: 'Какой дизайн вы предпочитаете?',
answers: [
{answer:'уникальный и лично свой',price:222,selected:1},
{answer:'у меня уже есть готовый дизайн',price:553,selected:2},
{answer:'дизайн не нужен',price:22,selected:3},
{answer:'купим тему на themeforest',price:442,selected:4}
],
select:1
},
{ id: '2',
question: 'есть ли у вас наработки по дизайну?',
answers: [
{answer:'да, есть',price:22,selected:0},
{answer:'нет, ничего нет',price:55,selected:1},
{answer:'дизайн не нужен',price:0,selected:2},
{answer:'еще крутой ответ',price:44,selected:3}
],
select:1
}
];
var ractive = new Ractive({
// The `el` option can be a node, an ID, or a CSS selector.
el: '#container',
// We could pass in a string, but for the sake of convenience
// we're passing the ID of the <script> tag above.
template: '#template',
// Here, we're passing in some initial data
data: {
designQuestions: designQuestions,
sum:0
},
onrender:function(options)
{
var self = this;
// proxy event handlers
self.on(
{
calc: function (e)
{
calc();
}
});
calc();
function calc()
{
var arrayLength = designQuestions.length;
sum = 0;
for (var i = 0; i < arrayLength; i++)
{
var lengthans = designQuestions[i].answers.length;
for (var j = 0; j < lengthans; j++) {
if (designQuestions[i].answers[j].selected === designQuestions[i].select){
sum = sum + designQuestions[i].answers[j].price;
}
}
}
self.set('sum',sum);
}
}
});
</script>
</body>
</html>

Related

why does vue recycle elements?

I'm using vue, and I found a bizarre behaviour while working on one of my projects.
When I update an array in javascript the items are put inside the old html elements (I suppose) so if these old elements have some particular attributes the new items are going to get them as well.
I'll put this example code (visually it sucks but that's not the point).
<head>
<title>Test</title>
<meta charset="UTF-8">
<script src="https://unpkg.com/vue#3" defer></script>
<script src="script.js" defer></script>
<style>
div[time-isselected="true"] {
background: rgb(0, 255, 0);
}
</style>
</head>
<body>
<div class="day container">
<div class="selection" v-for="day in daysList">
<input type="radio" class="radio-day" name="radio"
:id="returnTheInput(day)" :value="returnTheInput(day)" #click="setSelectedDay(day)">
<label :for="returnTheInput(day)">{{day}}</label>
</div>
</div>
<div class="hour-container">
<div v-for="hour in hoursList" class="hour" :id="returnTheInput(hour)" #click="setSelectedHour(hour)">
{{hour}}
</div>
</div>
</body>
Here's the script:
let daysList = ["mon 15","tue 16"];
let hoursList = [];
let selectedDay = undefined;
const valuesForTest = {
[daysList[0]]: ["10:00", "11:00"],
[daysList[1]]: ["15:00", "16:00"]
}
const { createApp } = Vue;
const vm = Vue.createApp({
data(){
return {
daysList: daysList,
hoursList: hoursList
};
},
methods: {
returnTheInput(input){
return input;
},
setSelectedDay(day){
selectedDay = day;
vm.hoursList.splice(0, hoursList.length); //Vue is reactive to splice
for(let i = 0; i < valuesForTest[selectedDay].length; i++){
vm.hoursList.push(valuesForTest[selectedDay][i]);
}
},
setSelectedHour(hour){
document.getElementById(hour).setAttribute("time-isselected", "true");
}
}
}).mount("body");
To see my point:
select a day
select an hour (click on it)
select the other day
By doing this the hour will still be selected, even though it will be from the new ones.
That's not what I had expected nor what I'd want. I thought the new items would be assigned to completely new html elements.
How do I avoid this? I could change the internal logic of my script, but I was wondering if there was another way. Ideally I'd want Vue to create new html elements for the new items (since I guess it's recycling the old ones).
There are at least 2 solutions for this.
The first is to assign an unique key to each child with the :key attribute:
let daysList = ["mon 15","tue 16"];
let hoursList = [];
let selectedDay = undefined;
const valuesForTest = {
[daysList[0]]: ["10:00", "11:00"],
[daysList[1]]: ["15:00", "16:00"]
}
const { createApp } = Vue;
const vm = Vue.createApp({
data(){
return {
daysList: daysList,
hoursList: hoursList
};
},
methods: {
returnTheInput(input){
return input;
},
setSelectedDay(day){
selectedDay = day;
vm.hoursList.splice(0, hoursList.length); //Vue is reactive to splice
for(let i = 0; i < valuesForTest[selectedDay].length; i++){
vm.hoursList.push(valuesForTest[selectedDay][i]);
}
},
setSelectedHour(hour){
document.getElementById(hour).setAttribute("time-isselected", "true");
}
}
}).mount("body");
<head>
<title>Test</title>
<meta charset="UTF-8">
<script src="https://unpkg.com/vue#3.2.37/dist/vue.global.js"></script>
<style>
div[time-isselected="true"] {
background: rgb(0, 255, 0);
}
</style>
</head>
<body>
<div class="day container">
<div class="selection" v-for="day in daysList">
<input type="radio" class="radio-day" name="radio"
:id="returnTheInput(day)" :value="returnTheInput(day)" #click="setSelectedDay(day)">
<label :for="returnTheInput(day)">{{day}}</label>
</div>
</div>
<div class="hour-container">
<div v-for="hour in hoursList" :key="hour" class="hour" :id="returnTheInput(hour)" #click="setSelectedHour(hour)">
{{hour}}
</div>
</div>
</body>
The second is to reset child elements then re-render them asynchronously with the nextTick utility:
let daysList = ["mon 15","tue 16"];
let hoursList = [];
let selectedDay = undefined;
const valuesForTest = {
[daysList[0]]: ["10:00", "11:00"],
[daysList[1]]: ["15:00", "16:00"]
}
const { createApp } = Vue;
const vm = Vue.createApp({
data(){
return {
daysList: daysList,
hoursList: hoursList
};
},
methods: {
returnTheInput(input){
return input;
},
setSelectedDay(day){
vm.hoursList = [];
selectedDay = day;
Vue.nextTick(() => {
vm.hoursList.splice(0, hoursList.length); //Vue is reactive to splice
for(let i = 0; i < valuesForTest[selectedDay].length; i++){
vm.hoursList.push(valuesForTest[selectedDay][i]);
}
});
},
setSelectedHour(hour){
document.getElementById(hour).setAttribute("time-isselected", "true");
}
}
}).mount("body");
<head>
<title>Test</title>
<meta charset="UTF-8">
<script src="https://unpkg.com/vue#3.2.37/dist/vue.global.js"></script>
<style>
div[time-isselected="true"] {
background: rgb(0, 255, 0);
}
</style>
</head>
<body>
<div class="day container">
<div class="selection" v-for="day in daysList">
<input type="radio" class="radio-day" name="radio"
:id="returnTheInput(day)" :value="returnTheInput(day)" #click="setSelectedDay(day)">
<label :for="returnTheInput(day)">{{day}}</label>
</div>
</div>
<div class="hour-container">
<div v-for="hour in hoursList" class="hour" :id="returnTheInput(hour)" #click="setSelectedHour(hour)">
{{hour}}
</div>
</div>
</body>

How to bind dynamic checkbox value to Knockout observableArray on an object?

I've posted my fiddle here, that has comments with it.
How can I convert/map the AllCustomers array into another array of Customer objects??
I need to push the checked checkboxes objects in to self.Customer(), {CustomerType,checked}
Then I would loop through list of Customer object Array and return an array of all checked Customers - self.CheckedCustomers
function Customer(type, checked)
{
var self = this;
self.CustomerType = ko.observable(type);
self.IsChecked = ko.observable(checked || false);
}
function VM()
{
var self = this;
//dynamically populated - this is for testing puposes
self.AllCustomers = ko.observableArray([
{
code: "001",
name:'Customer 1'
},
{
code: "002",
name:'Customer 2'
},
{
code: "003",
name:'Customer 3'
},
]);
self.selectedCustomers = ko.observableArray([]);
self.Customer = ko.observableArray([]);
//How can I convert the AllCustomers array into an array of Customer object??
//I need to push the checked object in to self.Customer(), {CustomerType,checked}
//uncomment below - just for testing looping through Customer objects
/*
self.Customer = ko.observableArray([
new Customer("001"),
new Customer("002")
]);
*/
// This array should return all customers that checked the box
self.CheckedCustomers = ko.computed(function()
{
var selectedCustomers = [];
ko.utils.arrayForEach(self.Customer(), function (customer) {
if(customer.IsChecked())
selectedCustomers.push(customer);
});
return selectedCustomers;
});
}
ko.applyBindings(new VM());
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<!-- ko foreach: AllCustomers -->
<input type="checkbox" data-bind="value: $data.code, checked:$parent.selectedCustomers" />
<span data-bind="text: $data.name"></span>
<!-- /ko -->
<br />
<h4>selectedCustomers code</h4>
<div data-bind="foreach: selectedCustomers">
<span data-bind="text: $data"></span>
</div>
<h4>Checked boxes are??</h4>
<div data-bind="foreach: CheckedCustomers">
<span data-bind="text: CustomerType"></span>
<span data-bind="text: IsChecked"></span>
<span>,</span>
</div>
<!-- Use this to loop through list of Customer object Array, uncomment below to test-->
<!--
<!-- ko foreach: Customer --
<input type="checkbox" data-bind="checked: IsChecked" />
<span data-bind="text: CustomerType"></span>
<!-- /ko --
-->
You're trying to convert object with properties code and name to an object of properties CustomerType and IsChecked. I'm assuming you want code to be mapped to CustomerType when creating new Customer object.
Here's a working jsfiddle for more or less what you wanted.
https://jsfiddle.net/s9yd0e7o/10/
Added the following code:
self.selectedCustomers.subscribe(function() {
self.Customer.removeAll();
ko.utils.arrayForEach(self.selectedCustomers(), function(item) {
self.Customer.push(new Customer(item, true));
});
});

Looping over an array of different objects in Knockout - Binding Error

I'm trying to render a different section of a page and apply the appropriate bindings for different items contained within a single array. Each item in the array could have a different structure / properties.
As an example we could have 3 different question types, the data associated with that question could be in a different format.
JSON Data
var QuestionTypes = { Textbox: 0, Checkbox: 1, Something: 2 }
var QuestionData = [
{
Title: "Textbox",
Type: QuestionTypes.Textbox,
Value: "A"
},
{
Title: "Checkbox",
Type: QuestionTypes.Checkbox,
Checked: "true"
},
{
Title: "Custom",
Type: QuestionTypes.Something,
Something: { SubTitle : "Something...", Description : "...." }
}
];
JavaScript
$(document).ready(function(){
ko.applyBindings(new Model(QuestionData), $("#container")[0]);
})
function QuestionModel(data){
var self = this;
self.title = ko.observable(data.Title);
self.type = ko.observable(data.Type);
self.isTextbox = ko.computed(function(){
return self.type() === QuestionTypes.Textbox;
});
self.isCheckbox = ko.computed(function(){
return self.type() === QuestionTypes.Checkbox;
});
self.isSomething = ko.computed(function(){
return self.type() === QuestionTypes.Something;
});
}
function Model(data){
var self = this;
self.questionData = ko.observableArray(ko.utils.arrayMap(data, function(question){
return new QuestionModel(question);
}));
}
HTML
<div id="container">
<div data-bind="foreach: questionData">
<h1 data-bind="text: title"></h1>
<!-- ko:if isTextbox() -->
<div data-bind="text: Value"></div>
<!-- /ko -->
<!-- ko:if isCheckbox() -->
<div data-bind="text: Checked"></div>
<!-- /ko -->
<!-- ko:if isSomething() -->
<div data-bind="text: Something">
<h1 data-text: SubTitle></h1>
<div data-text: Description></div>
</div>
<!-- /ko -->
</div>
</div>
The bindings within the if conditions get applied whether the condition if true / false. Which causes JavaScript errors... as not all of the objects within the collection have a 'Value' property etc.
Uncaught ReferenceError: Unable to process binding "foreach: function (){return questionData }"
Message: Unable to process binding "text: function (){return Value }"
Message: Value is not defined
Is there any way to prevent the bindings from being applied to the wrong objects?
Conceptual JSFiddle: https://jsfiddle.net/n2fucrwh/
Please check out the Updated Fiddler without changing your code.Only added $data in side the loop
https://jsfiddle.net/n2fucrwh/3/
<!-- ko:if isTextbox() -->
<div data-bind="text: $data.Value"></div>
<!-- /ko -->
<!-- ko:if isCheckbox() -->
<div data-bind="text: $data.Checked"></div>
<!-- /ko -->
<!-- ko:if isSomething() -->
<div data-bind="text: $data.Something"></div>
<!-- /ko -->
Inside the loop you need provide $data.Value.It seems to Value is the key word in knockout conflicting with the binding.
First of all your "QuestionModel" has no corresponding properties: you create "type" and "title" fields only from incoming data.
Proposed solution:
You can use different templates for different data types.
I've updated your fiddle:
var QuestionTypes = { Textbox: 0, Checkbox: 1, Something: 2 }
var QuestionData = [
{
Title: "Textbox",
Type: QuestionTypes.Textbox,
templateName: "template1",
Value: "A"
},
{
Title: "Checkbox",
Type: QuestionTypes.Checkbox,
templateName: "template2",
Checked: "true"
},
{
Title: "Custom",
Type: QuestionTypes.Something,
templateName: "template3",
Something: "Something"
}
];
$(document).ready(function(){
ko.applyBindings(new Model(QuestionData), $("#container")[0]);
})
function QuestionModel(data){
var self = this;
self.title = ko.observable(data.Title);
self.type = ko.observable(data.Type);
self.data = data;
}
function Model(data){
var self = this;
self.questionData = ko.observableArray(ko.utils.arrayMap(data, function(question){
return new QuestionModel(question);
}));
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script type="text/html" id="template1">
<div data-bind="text: Value"></div>
</script>
<script type="text/html" id="template2">
<div data-bind="text: Checked"></div>
</script>
<script type="text/html" id="template3">
<div data-bind="text: Something"></div>
</script>
<div id="container">
<div data-bind="foreach: questionData">
<h1 data-bind="text: title"></h1>
<!-- ko with: data -->
<!-- ko template: templateName -->
<!-- /ko -->
<!-- /ko -->
</div>
</div>
In the above edition you can get rid of "QuestionTypes".
Update 1
Of course, you can calculate template name from the question type.
Update 2
Explanation of the cause of errors. If you check original view model:
function QuestionModel(data){
var self = this;
self.title = ko.observable(data.Title);
self.type = ko.observable(data.Type);
self.isTextbox = ko.computed(function(){
return self.type() === QuestionTypes.Textbox;
});
self.isCheckbox = ko.computed(function(){
return self.type() === QuestionTypes.Checkbox;
});
self.isSomething = ko.computed(function(){
return self.type() === QuestionTypes.Something;
});
}
You can see, that "QuestionModel" has following properties: "title", "type", "isTextbox", "isCheckbox" and "isSomething".
So, if you will try bind template to "Value", "Checked" or "Something" you will get an error because view model does not contain such a property.
Changing binding syntax to the
<div data-bind="text: $data.Value"></div>
or something similar eliminates the error, but always will display nothing in this case.

There is any way like append more data in jquery?

I'm using knockoutjs to bind data, and I want to append data bind in one element.
Here is my code :
html:
<div id="wrapper">
<div data-bind="foreach: people">
<h3 data-bind="text: name"></h3>
<p>Credits: <span data-bind="text: credits"></span></p>
</div>
Javascript code:
function getData(pageNumber)
{
//code get data
//binding
ko.applyBindings({ peopla: obj }, document.getElementById('wrapper'));
}
In the first time the pageNumber is 1, then I call getData(1), and I want show more data in page 2 I will call getData(2), and in page 2 data will be show more in wrapper element like append in jquery.
If I use normal jquery I can call some like that
$("#wrapper").append(getData(2));
So I don't know how to use knockout bind more data in one elemnt
Try the following script, this sort of simulates how you can append data to your array by replacing existing data or adding on to it. Hope it helps
function myModel() {
var self = this;
self.people = ko.observableArray([{
name: 'Page 1 data',
credits: 'credits for page 1'
}]);
var i = 2;
self.getData = function () {
var returnedData = [{
name: 'Page ' + i + ' Name',
credits: 'credits for page ' + i
}];
self.people(returnedData);
i++;
}
self.getData2 = function () {
var returnedData = {
name: 'Page ' + i + ' Name',
credits: 'credits for page ' + i
};
self.people.push(returnedData);
i++;
}
}
ko.applyBindings(new myModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div id="wrapper">
<div data-bind="foreach: people">
<h3 data-bind="text: name"></h3>
<p>Credits: <span data-bind="text: credits"></span>
</p>
</div>
<button data-bind="click: getData">Simulate get data (Replaces current data)</button>
<button data-bind="click: getData2">Append to the same array (Adds to existing array)</button>

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