How can I access vue repeated item key from a method - vue.js

I have an html page, which has a table and an iterated row using v-for:
<table id="app-4">
<tr v-for="(item, index) in results" :key="item.id">
<td>#{{ item.title }}</td>
<td>
<button v-on:click="deleteItem(index)">Delete</button>
</td>
</tr>
</table>
and I have this js code.
var app4 = new Vue({
el: '#app-4',
data: {
results: []
},
methods: {
deleteItem: function (index) {
this.results.splice(index,1);
//Can I access item key and tr properties from here and the delete button
}
},
mounted() {
axios.get('api.list.url').then(response => {
this.results = response.data;
})
}
});
In the deleteItem function, Can I access item key and tr properties and append text to the item delete button.

The traditional Vue approach would probably be to use references
<table id="app-4">
<tr v-for="(item, index) in results" :key="item.id" ref="rows">
<td>#{{ item.title }}</td>
<td>
<button v-on:click="deleteItem(index)" ref="deleteButtons>
Delete
</button>
</td>
</tr>
</table>
And in the code
deleteItem: function (index) {
this.results.splice(index,1);
//Can I access item key and tr properties from here?
// Yes, e.g. to get the text content of the first cell
const text = this.$refs.rows[index].cells[0].textContent.trim();
// And add it to the delete button text
this.$refs.deleteButtons[index].textContent += " " + text;
}
Of course, that example is a bit nonsensical since you know the item's title, but the principle works for other properties of the text row (e.g. attributes, computed styles, classes, etc.)

Related

nested v-for loop not rendering

I am trying to display a table full of Orders and the corresponding Order details in a row-like fashion. However, I've run into a bit of a conundrum. Any geniuses out there? I've tried all sorts of variations in approaching this problem but I'm stuck. Someone mentioned that perhaps using a 'computed' property would do the trick but I haven't been able to get it to work >=/. A bit of help, maybe a lot of help, would be greatly appreciated!
<div id="MyOrdersApp" class="table-responsive">
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr style="color:black;font-size:2em;">
<th style="text-align:center;">Order Id</th>
<th style="text-align:center;">ProductIdsAndQuantity</th>
<th style="text-align:center;">Total Cost</th>
<th style="text-align:center;">Order Status</th>
<th style="text-align:center;">Shipment Tracking URL</th>
<th style="text-align:center;">Created Date</th>
</tr>
</thead>
<tbody>
<tr v-for="(order, i) in parsedOrders" style="background-color:snow;text-align:center;" class="align-content-center;">
<td>{{order.OrderId}}</td>
<td>
<ul>
<li v-if="order.CartLineItems != null" v-for="item in order.CartLineItems;" :key="uniqueKey">
<img :src="item.ProductImageUrl" />
<span>{{item.ProductName}} | </span>
<span>Quantity: {{item.Quantity}} | </span>
</li>
</ul>
</td>
<td style="color:black;font-size:1.5em;font-weight:bold;">{{order.TotalRevenue | currency }}</td>
<td>{{order.OrderStatus}}</td>
<td>{{order.ShipmentTrackingURL}}</td>
<td>{{order.CreatedDate | formatDate }}</td>
</tr>
</tbody>
</table>
</div>
<script>
var MyOrdersApp = new Vue({
el: '#MyOrdersApp',
data: {
parsedOrders: [],
theOrders: [],
uniqueKey: 0
},
computed: {
cartLineItems: function () {
return this.parsedOrders[0].CartLineItems;
}
},
methods: {
getMyOrders: function () {
var scope = this;
this.$http.get("https://" + location.host + "/Home/GetMyOrders").then(resp => {
if (resp.status == 200) {
this.theOrders = resp.body;
scope.setCartLineItems();
}
});
},
setCartLineItems: function () {
var scope = this;
var cartLineItems = [];
var cartDict = {};
for (var i = 0; i < this.theOrders.length; i++) {
cartDict = {};
cartLineItems = [];
cartDict = JSON.parse(this.theOrders[i].ProductIdsAndQuantity).CartLineItems;
for (var key in cartDict) {
var lineItem = JSON.parse(cartDict[key]);
cartLineItems.push(lineItem);
}
//this.allorders[i].CartLineItems = cartLineItems;
//scope.$set(this.allorders[i], 'CartLineItems', cartLineItems);
//this.theOrders[i].CartLineItems = cartLineItems;
scope.$set(this.theOrders[i], 'CartLineItems', cartLineItems);
}
this.parsedOrders = this.theOrders;
console.log("~~ Parsed Cart! ~~ ");
console.log(this.parsedOrders);
console.log(this.parsedOrders[1].CartLineItems);
}
},
mounted: function () {
var scope = this;
this.getMyOrders();
setTimeout(x => { scope.$forceUpdate(); scope.uniqueKey++; }, 1000);
}
});
</script>
How should I modify this to properly display the values in 'CartLineItems'??
Btw, parsedOrders looks like this:
If I comment out the inner v-for, the other columns display just fine and the table looks like this:
Some more background... when I fetch the orders via the $http call and the server returns the array of JSON, the ProductIdsAndQuantity property needs to be parsed as a JSON object and then set as it's own property on the order in question. The tricky part here is that the Vue component seems to not be reacting to the change in the order object array data. Hence, the need for a parsedOrders property and/or the use of scope.$forceUpdate(); or scope.uniqueKey++;. These were proposed solutions to the issue of the Vue component not re-rendering. However, these solutions are not working. So I'm stuck scratching my head...
You can't have both v-for and v-if on the same statement that's what is causing the issue. So instead try using computed in this case
<tbody>
<tr v-for="(order, i) in parsedOrders" style="background-color:snow;text-align:center;" class="align-content-center;">
<td>{{order.OrderId}}</td>
<td>
<ul>
<li v-for="(item, index) in getCartLineItems(order)" :key="index">
<img :src="item.ProductImageUrl" />
<span>{{item.ProductName}} | </span>
<span>Quantity: {{item.Quantity}} | </span>
</li>
</ul>
</td>
<td style="color:black;font-size:1.5em;font-weight:bold;">{{order.TotalRevenue | currency }}</td>
<td>{{order.OrderStatus}}</td>
<td>{{order.ShipmentTrackingURL}}</td>
<td>{{order.CreatedDate | formatDate }}</td>
</tr>
</tbody>
and in computed
computed: {
getCartLineItems() {
return order => {
if(order?.CartLineItems && order.CartLineItems.length) return order.CartLineItems;
return [];
}
}
}

Vue2: Can I pass an optional (global) filter into a reusable component?

I am quite new to Vue.
I am working on a table as component, which is supposed to be a lot of times. So far, so good, but now I want to use a filter, which can be optional passed into it.
That is how I "call" the table:
<table
:headers="headers"
:items="some.data"
></table>
data () {
return {
headers: [
{ title: 'date', value: ['date'], filter: 'truncate(0, 10, '...')' },
]
}
}
Here is my table component
<template>
<div>
<table class="table">
<thead>
<tr>
<!-- <th v-for="header in headers" :key="header.id" scope="col">{{ header.title }}</th> -->
<th v-for="header in headers" :key="header.id" scope="col">
{{ header.title }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.id" scope="col">
<td v-for="header in headers" :key="header.id" scope="row">
<!-- {{item}} -->
<span v-for="val in header.value" :key="val.id">
{{item[val] | truncate(0, 10, '...') }}
</span>
<!-- {{header.filter}} -->
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script >
export default {
name: 'Table',
props: {
headers: Array,
items: Array
}
}
</script>
My global filter:
Vue.filter('truncate', function (text, start, truncLength, clamp = '') {
return text.slice(start, truncLength) + clamp
// return text.slice(0, stop) + (stop < text.length ? clamp || ' ...' : '')
})
I was hopping, to add by that an optional filter (via v-if I would chech for it). So far I can render the filter as string ... but not execute it.
Even if I put the filter in the span, it does not work (it says then, "text.slice is not a function" in the console.
I was not successful with googling it, because with filters/filter it is mostly about how to use .filter(...) on data as JS method, but not as Vue filter.
Any advise?
A filter is a function that runs inside JSX template in html. Example of how to create custom Vue.js filter
Vue.filter('ssn', function (ssn) { // ssn filter
return ssn.replace(/(\d{3})(\d{2})(\d{4})/, '$1-$2-$3');
});
Using it
{{stringToTrans | ssn}}
To use a filter outside this you can use a computed property or standard function like so
convertedDateString() { // computed
return this.$moment(this.dateString).format('MM/DD/YYYY')
}
convertedDateString(dateString) { // method
return this.$moment(dateString).format('MM/DD/YYYY')
}

Updating a filtered multi-row Vuex backed form results in "do not mutate vuex store state"

I have a multi row form in Nuxt/Vuex that I am successfully CRUDing rows. I can create, updated, and delete rows. Sandbox here.
https://codesandbox.io/s/multirow-fields-hkzql
I have another form where I am filtering the rows based on a value in the row. I can create and delete rows but not update them. Sandbox here.
https://codesandbox.io/s/testing-filtered-multirow-fields-24cxb
When I edit a field in the filtered table I get the [vuex] do not mutate vuex store state outside mutation handlers error.
// store/Table.js snippet
export const getters = {
getField,
filteredRows: state => {
return state.rows.filter(row => row.key > 1);
}
};
<!-- component/MultiRow.vue snippet -->
<tr v-for="row in filteredRows" :key="row.key">
<td>
<input v-model="row.key">
</td>
<td>
<input v-model="row.value">
</td>
<td>
<button class="btn" #click="removeRow(row)">Remove row</button>
</td>
</tr>
Is there a solution in vuex-map-fields? Is there a solution without it?
Your are using Vuex in strict mode, so you cannot update the property of an object from the state outside a mutation, as explained here: https://vuex.vuejs.org/guide/forms.html
To solve your issue, firstcreate a new mutation method:
updateRow(state, row) {
const index = state.rows.findIndex(i => i.key === row.key);
if (index > -1) {
state.rows[index] = row;
}
}
Then replace your v-model by a :value + #inputin order to update the "row" value with this new mutation method:
<tbody>
<tr v-for="row in filteredRows" :key="row.key">
<td>
<input :value="row.key" #input="updateRow(row)">
</td>
<td>
<input :value="row.key" #input="updateRow(row)">
</td>
<td>
<button class="btn" #click="removeRow(row)">Remove row</button>
</td>
</tr>
</tbody>
And update your method list:
methods: {
...mapMutations("Table", ["addRow", "removeRow", "updateRow"])
}

How to process the emitted event from the component in v-for of Vuejs

I'm trying to process the emitted the event from the component rendered by the v-for.
For example,
I've made a combobox component that emits the event when changing the value.
It emits the event by this.$emit('item_change', item);.
I want to process this event for the corresponded user.
In the below code, I want to change the status value of the user when changing the value of the combobox of user.
It gets item as parameter when using v-on:item_change="status_change"
example
But it doesn't get the item as parameter in v-on:item_change="status_change(item , user)" though combobox emits the event with item, and status of user keeps the original value.
How could I solve this issue?
JSFiddle Example
<div id="mainapp">
<table>
<thead>
<th>Name</th><th>Status</th>
</thead>
<tbody>
<tr v-for="user in users">
<td>{{user.name}}</td>
<td><combobox v-bind:default="user.status" v-bind:data="status_codes" v-on:item_change="status_change(item, user)"></combobox></td>
</tr>
</tbody>
</table>
</div>
JS code
var combobox = Vue.component('combobox', {
data: function () {
return {
selected_item:{title:'Select', value:-1},
visible:false
}
},
props:['data','default','symbol'],
template: `
<div class="combobox">
<span class="symbol" v-if="!symbol">
<i class="fa fa-chevron-down" aria-hidden="true" ></i>
</span>
<span class="main" v-on:click="toggleVisible">{{selected_item.title}}</span>
<ul class="combodata" v-if="visible">
<li class="item" v-for="item in data" v-on:click="select(item)">{{item.title}}</li>
</ul>
</div>
`,
created:function(){
if(this.data.length>0){
if(this.default == null || this.default == undefined || this.default =='') this.default=0;
this.selected_item = this.data[this.default];
}
},
methods:{
toggleVisible:function(){
this.visible = !this.visible;
},
select:function(item){
if(this.selected_item != item){
this.selected_item= item;
this.$emit('item_change', item);
}
this.visible = false;
}
}
});
var app=new Vue({
el:"#mainapp",
data:{
status_codes:[{title:'Inactive', value:0},{title:'Active', value:1}],
users:[{name:'Andrew', status:1},{name:'Jackson', status:0},{name:'Tom', status:1}]
},
methods:{
status_change:function(item,user){ //This gets only the parameter from the event. How could I pass the additional parameters to this function?
console.log(item,user);
try{
user.status = item.value;
}catch(e){ console.log}
}
}
});
You need to pass $event to your status_change handler instead of item
<div id="mainapp">
<table>
<thead>
<th>Name</th>
<th>Status</th>
</thead>
<tbody>
<tr v-for="user in users">
<td>{{user.name}}</td>
<td>
<combobox v-bind:default="user.status" v-bind:data="status_codes" v-on:item_change="status_change($event, user)"></combobox>
</td>
</tr>
</tbody>
</table>
</div>
JSFiddle
See the Vue docs here about event handling:
Sometimes we also need to access the original DOM event in an inline statement handler. You can pass it into a method using the special $event variable
Use $event
What you need is actually v-on:item_change="status_change($event , user)".
When you do this.$emit('item_change', whatever);, whatever will become $event in the event listener.
https://jsfiddle.net/jacobgoh101/bLsw085r/1/
Try passing parameters to your function like this:
v-on:item_change="status_change(item, user)"
And in your function declaration, specify the parameters:
status_change: function (item, user) {
console.log(item, user);
}

vue.js does not correctly rerender compared to the vue instance object

I have a small issue with my vue template. The code is the following :
<template>
<div class="panel panel-default"
v-bind:id="'panel_'+noeud.id">
<div class="panel-heading">{{noeud.name}}</div>
<div class="panel-body">
<table class="table">
<thead>
<tr>
<th>Noeud</th>
<th>Poid</th>
</tr>
</thead>
<tbody>
<tr
v-for="noeud_poids in weightSorted"
v-if="noeud_poids.macro_zonning_noeud_id_2 != noeud.id"
is="macrozonningproximitenoeudpoids"
:noeud_poids="noeud_poids"
:noeud="noeud"
:noeuds="noeuds"
:delete_callback="delete_final"
:change_callback="update_line">
</tr>
<tr>
<td>
<select v-model="new_noeud">
<option value=""></option>
<option v-for="one_noeud in noeuds "
v-bind:value="one_noeud.id">{{one_noeud.name}}</option>
</select>
</td>
<td>
<input type="text" v-model="new_weight">
</td>
<td>
<input type="button" class="btn btn-primary" #click="addNoeudProximite" value="Ajouter"/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
export default {
props: ['pnoeud', 'pnoeuds'],
data: function(){
return {
points: 0,
points_restants: 100,
new_weight:0,
new_noeud:0,
noeud:this.pnoeud,
noeuds:this.pnoeuds,
weightSorted:this.pnoeud.weightSorted
}
},
mounted() {
},
methods:{
delete_final(macro_zonning_noeud_id_2){
axios.delete("/macrozonning/proximite/",{
params:{
macro_zonning_noeud_id_2:macro_zonning_noeud_id_2,
macro_zonning_noeud_id_1:this.noeud.id
}
}).then((res) => {
Vue.delete(this.weightSorted, String(macro_zonning_noeud_id_2));
})
},
update_line(nb_points){
this.points_restants = this.points_restants - nb_points;
this.points = this.points + nb_points;
},
addNoeudProximite(){
axios.put('/macrozonning/proximite/', {
'macro_zonning_noeud_id_1': this.noeud.id,
'macro_zonning_noeud_id_2': this.new_noeud,
'weight': this.new_weight
}).then((res) => {
Vue.set(this.weightSorted, String(this.new_noeud), res.data);
});
}
}
}
</script>
When the function delete_final is executed on the last item of my list, the view is correctly rerendered as the last item of my list is removed. But when I try to remove the first item of my list then the view rerenders but the the last item has been removed. When I check the Vue object in devtools, it does not reflect the new view, but it reflects the action taken (my first item has been removed).
If you have any idea where this problem comes from it would be awesome.
Thanks a lot community
Use a key attribute on the element you are rendering with v-for so that vue can exactly identify VNodes when diffing the new list of nodes against the old list. See key attribute
<tr> v-for="noeud_poids in weightSorted" :key="noeud_poids.id" </tr>