vue.js how to v-model values as separate arrays - vue.js

from the backend I'm getting an array like this.
then I render this array to a table like this
My code
<tr v-for="item in items">
<td>
{{item[1]}}
</td>
<td>
{{item[2]}}
</td>
<td>
<input type="text" v-model="grnItems[items[1]]"/>
</td>
</tr>
This is a purchase return component
what I want is v-model this each an every input element as a separate array along with the item name.
like this
[
["chicken","12"]
["chille","19"]
]
How do I achieve this using vue.js?

Use an auxiliar array with the data populated the way you want, some example using computed properties
new Vue({
el: '#app',
data: {
items: [['1', 'some text', '66'], ['2', 'another text', '12'], ['5', 'random text', '89']],
result: []
},
computed: {
procesedItems() {
return this.items.map(i => ({
id: i[0],
name: i[1],
amount: i[2]
}))
}
},
methods: {
doSomething() {
this.result = this.procesedItems.map(i => {
let aux = [];
aux.push(i.name, i.amount)
return aux
})
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<ul>
<li v-for="item in procesedItems"> {{item.id }} {{item.name }} <input v-model="item.amount"/></li>
</ul>
<button #click="doSomething">Calculate</button>
{{ result }}
</div>

Related

Vuejs + onClick doesn't work on dynamic element loaded after mounted

I'm stucked with this issue. When I click on some element it push an item to an array, and I show this array in a table. I want to add an action to delete any row of the table on this way for example:
Table
My code:
<div id="pos">
<div class="container-fluid" style="font-size: 0.8em;">
<div class="row grid-columns">
<div class="col-md-6 col">
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Descripcion</th>
<th>Stock</th>
<th>Precio uni</th>
<th>Precio alt</th>
<th>Cant</th>
<th>Subtotal</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<pos-products
:products="products"
v-on:remove-product="removeProduct"
>
</pos-products>
<!-- <tr v-for="item in products" :key="item.id">
<th scope="row">${item.id}</th>
<td>${ item.descripcion }</td>
<td>${ item.stock }</td>
<td>${ item.precio } $</td>
<td>${ item.precio_alt } $</td>
<td>
<v-minusplusfield :value="1" :min="1" :max="100" v-model="item.cant"></v-minusplusfield>
</td>
<td>${ getSubtotal(item) }</td>
<td> Borrar </td>
</tr> -->
</tbody>
</table>
</div>
<div class="col-md-6 col">
<div>
<div id="grid-header" class="p-2 border-b ">
<input class="form-control" name="searchString" placeholder="Buscar producto" type="text" v-model="searchString" />
</div>
</div>
<div style="background-color:#fff">
<div class="col-md-3" v-for="item in searchResults">
<a
href="#"
class="list-group-item"
:key="item.id"
#click="loadItem(item)"
>
<img src="//images03.nicepage.com/a1389d7bc73adea1e1c1fb7e/af4ca43bd20b5a5fab9f188a/pexels-photo-3373725.jpeg" alt="" class="u-expanded-width u-image u-image-default u-image-1" width="25" height="30">
<h6 class="u-text u-text-default u-text-1">${item.descripcion}</h6>
<h4 class="u-text u-text-default u-text-2">${item.precio}$ / ${item.precio_alt}$</h4>
</a>
</div>
</div>
</div>
</div>
</div>
Vue code:
const app = new Vue({
el: "#pos",
delimiters: ["${", "}"],
data () {
return {
products: [],
total: 0,
client: "",
user: "",
paymentDetail: [],
errors: {},
garantia: false,
saveButton: false,
seller: "",
searchString: "",
searchTypingTimeout: "",
searchResults: [],
}
},
methods: {
getSubtotal: function (item) {
return parseInt(item.cant) * parseFloat(item.precio);
},
loadItem: function (item) {
this.products.push({
id: item.id,
descripcion: item.descripcion,
stock: item.stock,
precio: item.precio,
precio_alt: item.precio_alt,
cant: 1,
});
},
removeItem: () => {
products = products.filter((el) => el !== item);
},
searchProducts: function (value) {
axios
.post("/v2/producto/search", {
query: value
})
.then((response) => {
if (!response.status == 200 || response.data.error) {
console.log('error')
const errorMessage = response.data.error
? response.data.error
: "Ha ocurrido un error";
console.log("mensaje: " + errorMessage);
this.$swal({
icon: "error",
title: "Oops...",
text: errorMessage,
});
return;
}
this.searchResults = response.data.data;
})
.catch((error) => {
console.log("catch error", error);
});
},
},
mounted() {
var csrf = document
.querySelector('meta[name="csrf-token"]')
.getAttribute("content");
this.products = [];
},
computed: {},
watch: {
total(val) {
this.total = parseFloat(val);
},
searchString(val) {
if (this.searchTypingTimeout) clearTimeout(this.searchTypingTimeout);
this.searchTypingTimeout = setTimeout(
() => this.searchProducts(this.searchString),
850
);
},
},
});
I got this:
vue.js?3de6:634 [Vue warn]: Property or method "removeItem" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
Try using classic function like this :
removeItem(item){
const index = this.items.findIndex(x => x.id === item.id)
this.items.splice(index, 1)
},
I've here loaded the data with the jsonplaceholder.typicode.com api
new Vue({
el: '#app',
data: () => ({
items: []
}),
async mounted(){
await axios.get('https://jsonplaceholder.typicode.com/posts')
.then(res => {
this.items = res.data
})
},
methods: {
removeItem(item){
const index = this.items.findIndex(x => x.id === item.id)
this.items.splice(index, 1)
},
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.27.2/axios.min.js" integrity="sha512-odNmoc1XJy5x1TMVMdC7EMs3IVdItLPlCeL5vSUPN2llYKMJ2eByTTAIiiuqLg+GdNr9hF6z81p27DArRFKT7A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div id="app">
<h1>List </h1>
<ul>
<li v-for="item of items" :key="item.id">
<a #click="removeItem(item)">{{item.id}} - {{item.title}}</a>
</li>
</ul>
</div>

Vue.js How to recalculate the displayed data after changing the select-option?

The task - when changing the unit of measurement, consider the total weight of the products.
Now the data in the array is changing, but new data is displayed on the screen only when click on other input fields
There is a table with data from the array, with the fields: Product name, net weight, quantity.
There is a select in the table with a choice of unit of measure - kg, grams, in order to understand in which units the "net weight" is set
for each value there is a conversion coefficient conversion_num from a separate array
Now, when changing the unit of measure, the unitChange function is launched, which changes the data in the array, but the changes are not displayed on the screen.
I tried with computed function, it work same.
What am I doing wrong?
HTML:
<tr v-for="(comp, i) in st">
<td>{{comp.name}}</td>
<td>
<input v-model.number="comp.weight_netto">
</td>
<td>
<select #change="unitChange($event.target.value, i)" v-model="comp.unit_id">
<option v-for="val in ul" :value="val.unit_id" v-text="val.name" ></option>
</select>
</td>
<td>
<input v-model.number="comp.quantity">
</td>
<td>{{itemqty(i)}}</td>
</tr>
JS:
methods: {
unitChange(value, i){
for (var j=0; j<this.ul.length; j++){
if (this.ul[j].unit_id==value){
this.st[i].conversion_num=this.ul[j].conversion_num;
break;
}
}
},
itemqty(i){
return (this.st[i].weight_netto*this.st[i].quantity)/this.st[i].conversion_num;
}
}
Don't mutate your products!
You'll end up with rounding errors: changing from one unit to another and back to initial will result in a different value from initial, because of the two roundings.
And it also makes things more difficult for you as everywhere you want to use the product you have to take into account its current unit.
A much cleaner solution is to leave the weight and unit unchanged (in the unit of your choice) and use a method to parse the displayed value, based on currently selected unit.
Your products remain unchanged (neither weight values or weight units change).
Proof of concept:
Vue.config.productionTip = false;
Vue.config.devtools = false;
new Vue({
el: '#app',
data: () => ({
products: [
{ id: 1, name: 'one', weight: 20},
{ id: 2, name: 'two', weight: 17}
],
units: [
{label: 'kgs'},
{label: 'lbs', factor: 2.20462},
{label: 'oz', factor: 35.274}
],
currentUnit: {label: 'kgs'}
}),
methods: {
convertToUnit(weight) {
return (Math.round(weight * (this.currentUnit.factor || 1) * 100) / 100) +
this.currentUnit.label
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
Current unit:
<select v-model="currentUnit">
<option v-for="(unit, key) in units" :value="unit" :key="key">{{ unit.label }}</option>
</select>
<div v-for="(product, k) in products" :key="k">
name: <span v-text="product.name"></span>,
weight: <span v-text="convertToUnit(product.weight)"></span>
</div>
</div>
You should use Vue.set
Also some things can be refactored
Example
const table = Vue.component('product-table', {
data() {
return {
products: [{
name: 'one',
weight_netto: 1.23,
quantity: 4,
conversion_num: .420,
unit_id: 1
},{
name: 'two',
weight_netto: 3.21,
quantity: 5,
conversion_num: .69,
unit_id: 2
}],
units: [{
unit_id: 1,
conversion_num: .420,
name: 'kg'
},
{
unit_id: 2,
conversion_num: .69,
name: 'lb'
}
]
}
},
template: `
<table>
<tbody>
<tr :key="product.name" v-for="product in products">
<td>{{product.name}}</td>
<td>
<input v-model.number="product.weight_netto">
</td>
<td>
<select #change="unitChange($event.target.value, product)" v-model="product.unit_id">
<option :key="unit.unit_id" v-for="unit in units" :value="unit.unit_id" v-text="unit.name" ></option>
</select>
</td>
<td>
<input v-model.number="product.quantity">
</td>
<td>{{itemqty(product)}}</td>
</tr>
</tbody>
</table>`,
methods: {
unitChange(unit_id, product) {
unit_id = Number(unit_id);
const { conversion_num } = this.units.find(pr => pr.unit_id === unit_id);
Vue.set(product, 'conversion_num', conversion_num);
},
itemqty({ weight_netto, quantity, conversion_num}) {
return ((weight_netto * quantity) / conversion_num).toFixed(2);
}
}
})
new Vue({
el: '#container'
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="container">
<product-table>
</product-table>
</div>

Vue,js, Computing a property with v-model?

I have a vue page that loads a bunch of actions from a database. It then creates a table with some data got from the action, using v-for.
Here is my problem: In one of the table rows I need a checkbox that is v-modeled to an attribute, action.weekly. Ideally, it will be a boolean. But there are a bunch of action entries in my database that don't have a weekly attribute. In those cases, I need the checkbox checked, as if it were true. Normally, I would use a computed property here, but you can't pass arguments to computed properties, so I don't know how to tell vue which action to look at at (I can't pass $event, ndx in to a computed property like I am doing below with handleEnableChanged() ).
Here is my code for the table:
<tbody>
<tr v-for="(action, ndx) in actions" >
<td class="pointer" #click='openModalCard(action, ndx)'>
{{action.name}}
</td>
<input type="checkbox" v-model="action.weekly??" #change="handleEnableChanged($event, ndx)"/>
</td>
<input type="checkbox" v-model="action.enabled" #change="handleEnableChanged($event, ndx)">
</td>
</tr>
</tbody>
In the cases where action does not have a weekly attribute, I want the checkbox checked as if it were true. How can I accomplish this?
If there is a better way to approach this, please let me know. I'm still a novice with vue.js.
I think it would be easiest to use v-if and v-else for this.. With that being said, I have also provided an example of how to handle this without v-if and v-else..
Using v-if and v-else:
new Vue({
el: "#root",
data: {
actions: [
{
name: "first",
weekly: true,
enabled: false
},
{
name: "second",
enabled: false
},
{
name: "third",
weekly: true,
enabled: true
},
{
name: "fourth",
enabled: true
}
]
},
template: `
<tbody>
<tr v-for="(action, ndx) in actions" >
<td class="pointer" #click='console.log(action, ndx)'>{{action.name}}</td>
<input v-if="'weekly' in action" type="checkbox" v-model="action.weekly" #change="handleEnableChanged($event, ndx)"/>
<input v-else type="checkbox" checked="true" #change="handleEnableChanged($event, ndx)"/>
</td>
<input type="checkbox" v-model="action.enabled" #change="handleEnableChanged($event, ndx)">
</td>
</tr>
</tbody>
`,
methods: {
handleEnableChanged(evt, ndx) {
console.log(evt, ndx);
},
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="root"></div>
Without v-if and v-else:
new Vue({
el: "#root",
data: {
actions: ""
},
template: `
<tbody>
<tr v-for="(action, ndx) in actions" >
<td class="pointer" #click='console.log(action, ndx)'>{{action.name}}</td>
<input type="checkbox" v-model="action.weekly" #change="handleEnableChanged($event, ndx)"/>
</td>
<input type="checkbox" v-model="action.enabled" #change="handleEnableChanged($event, ndx)">
</td>
</tr>
</tbody>
`,
methods: {
handleEnableChanged(evt, ndx) {
console.log(evt, ndx);
},
getActions() {
let originalActions = [
{
name: "first",
weekly: true,
enabled: false
},
{
name: "second",
enabled: false
},
{
name: "third",
weekly: true,
enabled: true
},
{
name: "fourth",
enabled: true
}
];
this.actions = originalActions.map(a => {
return {
name: a.name,
weekly: 'weekly' in a ? a.weekly : true,
enabled: a.enabled
}
})
}
},
created() {
this.getActions();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="root"></div>
You can just do this:
<input type="checkbox" :checked="!!action.weekly">
The !! operator will return true if the value is not undefined or empty
If you want all items in the array to have an action.weekly property, then update your data when it is initially retrieved. You can then just use v-model="action.weekly" for all items in the array.
this.newArray = this.oldArray.map(item => {
if(!item.hasOwnProperty('weekly')){
item.weekly = true;
}
return item
})

create unique key in vue v-for loop

following some research across the web, i understand i should add index to the loop and then add it as a key.
how would you suggest creating a unique key for both td's in the following code:
<template v-for="lesson in lessons">
<td #click="sort(lesson.questions)" :key="lesson.lessonId">
questions
</td>
<td #click="sort(lesson.grade)" :key="lesson.lessonId">
grade
</td>
</template>
the only idea i had was to add index to the loop and then have the second index as follows:
:key="`${lesson.lessonId}+1`"
but that feels a bit odd and error prone, am i right?
There are 2 ways,
first is add the static number as you mentioned:
:key="`${lesson.lessonId}567`"
Second is generate a new ID, and you will using uuid version 4 package, that will generate random id for you,
<template>
:key="generateID"
</template>
<script>
const uuidv4 = require('uuid/v4');
module.exports = {
data: function () {
return {
generateID: uuidv4();
}
}
}
</script>
The special attribute 'key' can be either 'numeric' or 'string', to solve the problem you can prefix your lessonId with a string
<template v-for="lesson in lessons">
<td #click="sort(lesson.questions)" :key="`question_${lesson.lessonId}`">
questions
</td>
<td #click="sort(lesson.grade)" :key="`grade_${lesson.lessonId}`">
grade
</td>
</template>`
<ul id="example-2">
<li v-for="(item, index) in items" :key="key(index, item)">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
</ul>
var example2 = new Vue({
el: '#example-2',
data: {
parentMessage: 'Parent',
items: [
{ message: 'Foo' },
{ message: 'Bar' }
]
},
methods: {
key (index, item) {
return `${index}${item.message}`
}
}
})

v-if and v-else not working after data array change

Depending on the array 'r.meta.fields' a specific sort icon of each column needs to be shown. When the template is rendering, it is working correctly. But when the array change, the template isn't changing anymore.
<th v-for="field in r.meta.fields">
{{field.label}}
<a href="#" #click.prevent="sortField(field)">
<div class="fa fa-sort-up" v-if="field.sort_direction === 'desc'"></div>
<div class="fa fa-sort-down" v-else-if="field.sort_direction === 'asc'"></div>
<div class="fa fa-sort" v-else-if="field.sortable"></div>
</a>
What could be the problem?
You could create a mapping for the sort icons and handle the changes on click:
const vm = new Vue({
el: '#app',
data() {
const iconMap = {
sort: {
'asc': 'fa-sort-up',
'desc': 'fa-sort-down'
}
};
return {
r: {
meta: {
fields: [
{
label: 'field #1',
sortable: false,
sort_direction: 'asc',
icon: ''
},
{
label: 'field #2',
sortable: true,
sort_direction: 'desc',
icon: iconMap.sort['desc']// Initially sortable in descending order
}
]
}
},
iconMap
}
},
methods: {
sortField(field) {
let direction = (field.sort_direction === 'asc') ? 'desc' : 'asc';
let icon = this.iconMap.sort[direction] || '';
field.sort_direction = direction;
field.icon = icon;
}
}
})
Template or HTML
<div id="app">
<table>
<tr>
<th v-for="field in r.meta.fields" :key="field.label">
{{field.label}}
<a href="#"
:class="field.icon"
#click.prevent="sortField(field)"></a>
</th>
</tr>
</table>
</div>
if you are using something like
r.meta.fields = newValue
then this won't work.
you should use
Vue.set(r.meta.fields, indexOfItem, newValue)
document here: vue update array