I'm trying to calculate a value from 2 input boxes and then get the total of those input boxes. I'm then trying to get the all my amounts and total them and add them to the subtotal but the issue I'm having is that when I type in a number in the first box my output is NaN instead of 0 and I would like for it to show me a 0 instead.
Here is my code
<template>
<div class="content-header">
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-lg-12">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Unit</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr v-for="product in products">
<td>{{ product['name'] }}</td>
<td>
<input type="text" class="form-control" v-model="unit[product['unit']]" #change="calculateCost(product['name'])">
</td>
<td>
<input type="text" class="form-control" v-model="price[product['price']]" #change="calculateCost(product['name'])">
</td>
<td>
{{ cost[product['name']] }}
</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td>
Subtotal: {{ subTotal }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default{
props: [],
data(){
return {
products: [],
unit: {},
price: {},
cost: []
}
},
computed:{
subTotal(){
if(this.cost!== null)
{
if(Object.keys(this.cost).length !== 0){
return Object.keys(this.cost).reduce((carry, item) => {
carry+= Number(this.cost[item])
return carry;
}, Number(0))
}else{
return 0;
}
}
}
},
methods: {
getProducts(){
axios.get(`/api/product/all`).then(response => {
this.products = response.data.products;
});
},
calculateCost(item){
this.cost[item] = Number(this.unit[item]) * Number(this.price[item]);
},
},
mounted() {
this.getProducts();
}
}
</script>
Almost all type of inputs return a string. You can use
<input type="number" class="form-control" v-model="unit[product['unit']]" #change="calculateCost(product['name'])">
or
<input type="text" class="form-control" v-model.number="unit[product['unit']]" #change="calculateCost(product['name'])">
The problem is the v-model for unit and price are set to the different keys than the one given to calculateCost(), which causes the lookups to fail and results in NaN:
<input v-model="unit[product['unit']]" #change="calculateCost(product['name'])"> ❌
^^^^^^ ^^^^^^
<input v-model="price[product['price']]" #change="calculateCost(product['name'])"> ❌
^^^^^^^ ^^^^^^
<input v-model="unit[product['name']]" #change="calculateCost(product['name'])"> ✅
<input v-model="price[product['name']]" #change="calculateCost(product['name'])"> ✅
Setting the keys to product['name'] ensures the correct lookup for unit and price in calculateCost(). Since the user could enter invalid values (non-numeric strings) or omit a value, it's possible to get NaN, so it would be a good idea to add a NaN-check in calculateCost():
calculateCost(item) {
const unit = Number(this.unit[item]);
const price = Number(this.price[item]);
if (Number.isNaN(unit) || Number.isNaN(price)) return;
this.cost[item] = unit * price;
},
Also, you probably want the cost to react to user input, so switch from #change to #input:
<input v-model="unit[product['name']]" #input="calculateCost(product['name'])">
<input v-model="price[product['name']]" #input="calculateCost(product['name'])">
demo
Related
I am very new to vue and I am trying to validate an array using vuelidate which is used to render a dynamic table. The problem is with the validation() method as I can comprehend.
According to vuelidate docs, https://vuelidate.js.org/#sub-collections-validation, the $each method supports array. When I use it, the validation never fails. However, when I omit $each, and, try to validate first index of the array, it returns as corrected - validation fails.
To be more precise, I am trying to validate each added row(s), and if there's a problem with the validation, it'd add a class to the affected row.
Component App.vue,
This is the HTML code:
<template>
<div>
<br>
<div class="text-center">
<button type="button" class="btn btn-outline-primary" #click="addRow()">Shto</button>
</div>
<br>
<p
v-for="error of v$.$errors"
:key="error.$uid"
>
<strong>{{ error.$validator }}</strong>
<small> on property</small>
<strong>{{ error.$property }}</strong>
<small> says:</small>
<strong>{{ error.$message }}</strong>
</p>
<form name="articles" #submit.prevent="submitForm">
<table class="table table-hover table-responsive">
<thead class="thead-inverse">
<tr>
<th>Nr.</th>
<th class="col-3">Numri</th>
<th class="col-4">Përshkrimi</th>
<th class="col-1">Sasia</th>
<th class="col-1">Çmimi</th>
<th class="col-2">Shuma</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(item, idx) in items_table" :key="item">
<td>
{{idx+1}}
</td>
<td>
<input v-model="item.part_no" name="part_no[]" class="form-control" type="text" />
</td>
<td>
<textarea v-model="item.part_name" name="part_name[]" class="form-control" type="text"></textarea>
</td>
<td>
<input v-model="item.part_qty" name="part_qty[]" class="form-control " type="number" step="0.01">
</td>
<td>
<input v-model="item.part_price" name="part_price[]" class="form-control" type="number" step="0.01">
</td>
<td>
<input :value="item.part_total" name="part_total[]" class="form-control text-center border-0" style="background-color: transparent; font-size: 18 px;" type="text" disabled>
</td>
<td>
<button type="button" #click="deleteRow(idx)" class="btn btn-danger btn-md btn-block">X</button>
</td>
</tr>
</tbody>
</table>
<div class="text-center">
<button type="submit" name="" id="" class="btn btn-primary btn-lg btn-block">Valido</button>
</div>
</form>
<div class="text-center">
<textarea name="" id="verbose_log" cols="70" rows="15" refs="logg"></textarea>
</div>
</div>
</template>
This is the content from script tag:
<script>
import useVuelidate from '#vuelidate/core'
import { required } from '#vuelidate/validators'
export default {
name: 'App',
setup: () => ({ v$: useVuelidate() }),
validation() {
return {
items_table: {
$each: {
part_no: {
required,
}
}
},
}
},
data() {
return {
items_table: [
{
part_no: '', part_name: '', part_qty: '', part_price: '', part_total: ''
}
],
items_subtotal: 0.00,
items_total: 0.00,
}
},
methods: {
deleteRow(index) {
if(this.items_table.length == 1) {
this.items_table.splice(index, 1);
this.items_subtotal = 0.00;
this.items_total = 0.00;
this.addRow();
} else if(this.items_table.length > 0) {
this.items_table.splice(index, 1);
}
},
addRow() {
this.items_table.push({part_no: '', part_name: '', part_qty: '', part_price: '', part_total: ''});
},
submitForm() {
this.v$.$touch();
//if (this.v$.$error) return;
console.log(this.v$);
document.getElementById('verbose_log').innerHTML = JSON.stringify(this.$data, null, 4);
}
},
computed: {
//
}
}
</script>
For the sake of clarity, I have excluded two methods which calculate line total and the total itself.
I'm trying to create a form that will update a product's pricing when adding the unit amount of the product and
a price of the product, but when I type into one of my textboxes I end up getting this error
The specified value "unit_product_1" cannot be parsed, or is out of range
I don't know what that means or how to solve it.
Here is my code
<template>
<div class="content-header">
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-lg-12">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Unit</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr v-for="product in products">
<td>{{ product['name'] }}</td>
<td>
<input type="text" class="form-control" v-model="product['unit']">
</td>
<td>
<input type="text" class="form-control" v-model="product['price']">
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default{
props: [],
data(){
return {
products: [],
unit_product_1: null,
price_product_1: null,
}
},
computed:{
},
methods: {
getProducts(){
axios.get(`/api/product/all`).then(response => {
this.products = response.data.products;
});
}
},
mounted() {
this. getProducts();
}
}
</script>
unit_product_1: null,
price_product_1: null
You don't use them anywhere.
If you want to mutate your incoming products, you should make a new POST request and send the data back to the server. In this scenario you must know how the server waits to receive the data and send them back as requested.
How to make only one element activate vue js?
I have 3 drop-down lists, 3 are activated at once, how do I make sure that only one is activated?
As far as I understand, this needs to be done through a loop, but this framework is not given to me
<tr class="inputs-table">
<td>Type object: </td>
<td>
<div class="select">
<div class="select-header form-control" v-on:click="AddForm">
<span class="select__current">Please select one option</span>
<span class="select__off">х</span>
</div>
<addForm v-if="addedForm" />
</div>
</td>
</tr>
<tr class="inputs-table">
<td>Type business-model: </td>
<td>
<div class="select">
<div class="select-header form-control" v-on:click="AddForm">
<span class="select__current">Please select one option</span>
<span class="select__off">х</span>
</div>
<addForm v-if="addedForm"/>
</div>
</td>
</tr>
import addForm from './modal/addForm.vue';
export default {
name: 'Index',
data() {
return {
addedForm: false
}
},
methods: {
AddForm(){
this.addedForm = true;
},
closeForm() {
this.$parent.addedForm = false;
}
},
components: {
addForm,
}
}
with your question and the given screenshot on the comment section, it seems you implement dropdown list in your addForm component and when you click on <div class="select-header form-control" v-on:click="AddForm"> in "type object" or "Type business-model" component will expand the addForm component and your problem is when you click on one header both addForm components are visible.
In that case, there may be several methods to fix this. One easy way to add numbering to each component and only activate the addForm if number equals.
<tr class="inputs-table">
<td>Type object: </td>
<td>
<div class="select">
<div class="select-header form-control" v-on:click="AddForm(1)">
<span class="select__current">Please select one option</span>
<span class="select__off">х</span>
</div>
<addForm v-if="addedForm === 1" />
</div>
</td>
</tr>
<tr class="inputs-table">
<td>Type business-model: </td>
<td>
<div class="select">
<div class="select-header form-control" v-on:click="AddForm(2)">
<span class="select__current">Please select one option</span>
<span class="select__off">х</span>
</div>
<addForm v-if="addedForm === 2"/>
</div>
</td>
</tr>
import addForm from './modal/addForm.vue';
export default {
name: 'Index',
data() {
return {
addedForm: 0
}
},
methods: {
AddForm(number){
this.addedForm = number;
},
closeForm() {
this.$parent.addedForm = false;
}
},
components: {
addForm,
}
}
Since you are using 3 form element this would be enough but if you are planning to use a dynamic number of components it would be great if you use for a method such as create list for each type object
items: [{id:1, title: 'Type Object'}, {id:2, title: 'Business Model'}]
Then use v-for in your tr component such as,
<tr class="inputs-table" v-for="item in items" key="item.id">
<td>{{item.title}}: </td>
<td>
<div class="select">
<div class="select-header form-control" v-on:click="AddForm(item.id)">
<span class="select__current">Please select one option</span>
<span class="select__off">х</span>
</div>
<addForm v-if="addedForm === item.id"/>
</div>
</td>
</tr>
I want to have v-model and value set in one input. Here is my code:
<td>
<input v-model="invoice.items[n-1].net_price" type="number" step="0.01" min="1" form="invoice-form">
</td>
<td v-if="invoice.items[n-1].net_price && invoice.items[n-1].quantity">
<input :value="(invoice.items[n-1].net_price * invoice.items[n-1].quantity).toFixed(2)" type="text" form="invoice-form" readonly>
</td>
<td v-else>
<input type="text" readonly>
</td>
Problem is with 2nd input. Now it works like this:
You input value in net-price input
You input quantity
It calculates a value and set it in readonly input
Now I want to set v-model in this 2nd input, but when I try to do this I get an error.
Is there a way to set a model to this calculated value?
A more robust approach is to perhaps put your calculation logic in a method and use destructuring assignment to unpack the values off the individual items. In addition, the variables can also be assigned default values in the case that the unpacked values are undefined -- That way, you no longer need to worry about the computed values returning NaN since they will fallback to zeros when they are.
Try something like that:
new Vue({
data: () => ({
invoice: {
items: [
{
net_price: 30.05,
quantity: 4
},
{
net_price: 14.99,
quantity: 7
},
]
}
}),
methods: {
calculate({ net_price = 0, quantity = 0}) {
return (net_price * quantity).toFixed(2);
}
}
})
.$mount('#app');
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<form id="invoice-form">
<table>
<thead>
<tr>
<th>Net Price</th>
<th>Quantity</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) of invoice.items" :key="index">
<td>
<input v-model="item.net_price" type="number" step="0.01" min="1" form="invoice-form" />
</td>
<td>
<input v-model="item.quantity" type="number" min="0" form="invoice-form" />
</td>
<td>
<input :value="calculate(item)" form="invoice-form" readonly />
</td>
</tr>
</tbody>
</table>
</form>
</div>
The rows of a table are generated using a v-for loop over an array of objects in graphicState. I am trying to create a column of radio buttons. When a radio button is checked, this should set graphicState[index].selected to true.
This post is interesting, but how can I use it set graphicState[index].selected to true?
<form>
<div class="row">
<div class="col-md-12 " align="center">
<table class="table-striped" v-on:mouseleave="mouseleave()">
<thead>
<tr>
<th></th>
<th>Show</th>
<th>Select</th>
<th>Shape</th>
</tr>
</thead>
<tbody>
<tr v-for="(form, index) in graphicState" :key="index">
<td #click="removeDate(index)"><i class="far fa-trash-alt"></i></td>
<td>
<input type="checkbox" v-model="form.show">
</td>
<td>
<input type="radio" name="grp" id="grp" value="true" v-model="form.selected">
</td>
<td v-on:click="toggleShape(index)">
{{ form.shape }}
</td>
</tr>
</tbody>
</table>
<div v-for="(form, index) in graphicState " :key="index">
</div>
</div>
</div>
</form>
You can use #change event handler:
<input type="radio" name="grp" id="grp" value="true" v-model="form.selected" #change="onChange($event, index)">
then handle in method:
methods: {
onChange (event, index) {
this.graphicState[index].selected = event.target.value
this.graphicState = JSON.parse(JSON.stringify(this.graphicState))
}
}
The code you have already should set the graphicState[index].selected to true for the radio inputs, but the input values (and thus graphicState[index].selected through v-model) are never set to false, which is a problem if the user is allowed to change their mind to select a different input. This occurs because the radio input's change-event is only fired when the its checked property is set to a truthy value (upon selection).
One solution is to add a change-event handler that clears the .selected value for the non-selected inputs.
// template
<tr v-for="(form, index) in graphicState">
<input #change="onRadioChange(index)" ...>
</tr>
// script
onRadioChange(selectedIndex) {
this.graphicState
.filter((x,i) => i !== selectedIndex) // get non-selected inputs
.forEach(x => x.selected = false)
}
But there's still another problem if you're using HTMLFormElement's native submit. In the following template, when the v-model value is true, Vue sets the radio input's checked property to true, which tells the HTMLFormElement to use this particular input's value as the group value...
<input type="radio"
name="grp"
v-model="form.selected"
value="true">
All the radio inputs have the same value, so the receiver of the form data won't be able to tell which input is selected. To address this, assign a unique value to each input based on the iterator item. For example, you could use ID:
<input type="radio"
name="grp"
v-model="form.selected"
value="form.id">
new Vue({
el: '#app',
data() {
return {
graphicState: [
{id: 'A', selected: false, show: false},
{id: 'B', selected: false, show: false},
{id: 'C', selected: false, show: false},
]
}
},
methods: {
mouseleave() {},
removeDate() {},
toggleShape() {},
onRadioChange(selectedIndex) {
this.graphicState
.filter((x,i) => i !== selectedIndex)
.forEach(x => x.selected = false)
},
}
})
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<script src="https://unpkg.com/vue#2.6.8/dist/vue.min.js"></script>
<div id="app">
<form method="post" action="//httpbin.org/post">
<div class="row">
<div class="col-md-12" align="center">
<table class="table-striped" v-on:mouseleave="mouseleave()">
<thead>
<tr>
<th></th>
<th>Show</th>
<th>Select</th>
<th>Shape</th>
</tr>
</thead>
<tbody>
<tr v-for="(form, index) in graphicState" :key="form.id">
<td #click="removeDate(index)"><i class="far fa-trash-alt"></i></td>
<td>
<input type="checkbox" v-model="form.show">
</td>
<td>
<input type="radio" name="grp" :value="form.id" v-model="form.selected" #change="onRadioChange(index)">
</td>
<td v-on:click="toggleShape(index)">
{{ form.shape }}
</td>
</tr>
</tbody>
</table>
<pre>{{graphicState}}</pre>
</div>
</div>
<button>Submit</button>
</form>
</div>