display a foreign data in a table vuejs - vue.js

I am creating a page, in which a section called product, I show data and in this I have a data called id_categoria, where instead of showing the id I want to show the name of that category in question, this data is in the table of categories and in the product table I have the id. I already managed to show the data, but not this, besides that I managed to save and edit it in question, it is only necessary in show.
When working with vuejs I saw that you have to use a v-if but I do not know how to do it, or at least the attempts I have made have been wrong.
this is the code of the table where I show the data
<div class="card-body table-responsive">
<table id="example1" class="table table-bordered table-striped">
<thead>
<tr>
<th>id_producto</th>
<th>imagen</th>
<th>código</th>
<th>producto</th>
<th>categoria</th>
<th>stock</th>
<th>precio_compra</th>
<th>precio_venta</th>
<th>fecha</th>
<th colspan="2" class="text-center">Acciones</th>
</tr>
</thead>
<tbody>
<tr v-for="pro in productos" :key="pro.id_productos">
<th>{{pro.id_productos}}</th>
<td>
<img :src="pro.imagen" class="img-circle elevation-2" alt="Product Image" width="60" />
</td>
<td>{{pro.codigo}}</td>
<td>{{pro.producto}}</td>
<td>{{pro.id_categoria}}</td>
<td>{{pro.stock}}</td>
<td>{{pro.precio_compra}}</td>
<td>{{pro.precio_venta}}</td>
<td>{{pro.fecha}}</td>
<td class="text-center">
<button #click="modificar=true; abrirModal(pro)" type="button" class="editar btn btn-primary"><i class="fa fa-pencil-alt"></i></button>
</td>
<td class="text-center">
<button #click="eliminar(pro.id_productos,pro.producto)" type="button" class="eliminar btn btn-danger" data-toggle="modal" data-target="#modalEliminar"><i class="fas fa-dumpster-fire"></i></button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<th>id_producto</th>
<th>imagen</th>
<th>código</th>
<th>producto</th>
<th>categoria</th>
<th>stock</th>
<th>precio_compra</th>
<th>precio_venta</th>
<th>fecha</th>
<th colspan="2" class="text-center">Acciones</th>
</tr>
</tfoot>
</table>
</div>
and this is the script to bring the info to me
<script>
import axios from "axios";
export default {
watch: {
$route: {
immediate: true,
handler(to, from) {
document.title = to.meta.title || 'Productos';
}
},
},
data() {
return{
productosdatos:{
id_producto: "",
codigo: "",
producto: "",
stock: "",
precio_compra: "",
precio_venta : "",
id_categoria: "",
},
id: 0,
modificar: true,
modal: 0,
tituloModal: '',
productos:[],
categorias:[],
}
},
methods: {
async listarcategorias(){
const res = await axios.get('http://localhost:8000/categoria/');
this.categorias = res.data;
console.log(this.categorias)
},
async listar(){
const res = await axios.get('http://localhost:8000/productos/');
this.productos = res.data;
console.log(this.productos)
},
cerrarModal(){
this.modal = 0;
}
},
created() {
this.listar();
}
}
</script>
as you can see I have a variable called products, where is the id_category corresponding to the product, and categories where I bring all the category info.
the table looks something like this:
how can I make it not show the id of the category but the name of the category in question ?
pd: the category data is received in json as follows:
{
"id_categoria": 8,
"categoria": "Electrodomesticos",
"fecha": "2021-10-24 13:55:00"
}
thank you if you can help me to show the name of the category thank you

You can implement a function like this:
const findCategory = (id)=>{this.categorias.find(category=>category.id_categoria===id)?.categoria}
And in the template:
<td>{{findCategory(pro.id_categoria)}}</td>

Only one small change. In your code you have to edit. pro.id_categoria to pro.categoria. see comment inline.
<tr v-for="pro in productos" :key="pro.id_productos">
<th>{{pro.id_productos}}</th>
<td>
<img :src="pro.imagen" class="img-circle elevation-2" alt="Product Image" width="60" />
</td>
<td>{{pro.codigo}}</td>
<td>{{pro.producto}}</td>
// this line
<td>{{pro.id_categoria}}</td>
// edit to
<td>{{ pro.categoria }} </td>

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 [];
}
}
}

Vue Component data object property's behavior not as expected [Solved]

I have an app with a child component that makes a call to an api for a player's season stats. You click on the players name and I emit click event to child from Parent component. The problem is when you click on Players name from parent all instances of the child component are revealed. I just want the one player. I thought because I have a showComponent instance for each child by toggling this.showComponent in child would get my expected behavior but no.
Code:
Parent-
methods: {
emitPlayerSeasonStatsClicked: function(event) {
const target = event.target;
EventBus.$emit("showPlayerTemplateClicked", target);
}
},
template: `
<div v-for="playerStats in props_box_game_scores[index].data.gameboxscore.awayTeam.awayPlayers.playerEntry">
<tr v-if="playerStats.player.Position === 'P'" class="d-flex" v-bind:data-player-id="playerStats.player.ID">
<td class="col-4 justify-content-center" scope="row" title="Click for Season Stats">
{{playerStats.player.FirstName}} {{playerStats.player.LastName}}
<span v-if="playerStats.stats.Wins['#text'] === '1'">(W)</span>
<span v-else-if="playerStats.stats.Losses['#text'] === '1'">(L)</span>
<span v-else-if="playerStats.stats.Saves['#text'] === '1'">(S)</span>
</td>
<td class="col-2 justify-content-center" justify-content="center">
{{playerStats.stats.InningsPitched['#text']}}</td>
<td class="col-2 justify-content-center">{{playerStats.stats.RunsAllowed['#text']}}</td>
<td class="col-2 justify-content-center">{{playerStats.stats.PitcherStrikeouts['#text']}}</td>
<td class="col-2 justify-content-center">{{playerStats.stats.EarnedRunAvg['#text']}}
</td>
</tr>
<pitcher-season-stats v-bind:props_player_id="playerStats.player.ID"></pitcher-season-stats>
</div>
Child-
cumlativeStats: Vue.component("player-season-stats", {
props: ["props_player_id"],
data: function() {
return {
Hits: "",
HR: "",
RBI: "",
BattingAvg: "",
showComponent: false
};
},
mounted: function() {
EventBus.$on("showPlayerTemplateClicked", function(data) {
this.showComponent = !this.showComponent;
});
},
methods: {
retrievePlayerStats: function(playerId) {
const url = `https://api.mysportsfeeds.com/v1.2/pull/mlb/2019-regular/cumulative_player_stats.json?player=`;
const params = {
playerstats: "AB,H,HR,RBI,AVG",
force: true
};
...
template: `
<tr class="d-flex" v-if:showComponent>
<td #click="retrievePlayerStats(props_player_id)" class="col-4 justify-content-center" scope="row">
Season Stats</td>
</td>
<td class="col-2 justify-content-center" justify-content="center">
{{ Hits }}</td>
<td class="col-2 justify-content-center">{{ HR }}</td>
<td class="col-2 justify-content-center"> {{ BattingAvg }}</td>
<td class="col-2 justify-content-center">{{ RBI }}</td>
</tr>
` // End template
})
Any suggestions welcome. Sorry for the formatting.
**
Updated Working Solution:
**
Parent:
methods: {
emitPlayerSeasonStatsClicked: function($event) {
let playerId = $event.target.dataset.playerId;
EventBus.$emit("showPlayerTemplateClicked", playerId);
}
}
....
<table #click="emitPlayerSeasonStatsClicked($event)" class="table table-striped table-bordered table-hover table-sm collapse" v-bind:class="'multi-collapse-' + index">
<tr v-if="playerStats.stats.AtBats['#text'] > 0" class="d-flex">
<td class="col-4 justify-content-center" :data-player-id='playerStats.player.ID' scope="row" title="Click for Season Stats">
{{playerStats.player.FirstName}} {{playerStats.player.LastName}} ({{playerStats.player.Position}})</td>
Child:
mounted: function() {
EventBus.$on(
"showPlayerTemplateClicked",
this.onShowPlayerTemplateClicked
);
},
methods: {
onShowPlayerTemplateClicked: function(playerId) {
if (playerId === this.props_player_id) {
this.loading = true;
this.showComponent = !this.showComponent;
this.retrievePlayerStats(playerId);
}
},
template: `
<transition name="fade">
<tr class="d-flex" v-if="showComponent">
<td v-if="!loading" class="col-4 justify-content-center" scope="row">
Season Stats</td>
</td>
<td class="col-2 justify-content-center" justify-content="center">
{{ Hits }}</td>
<td class="col-2 justify-content-center">{{ HR }}</td>
<td class="col-2 justify-content-center"> {{ BattingAvg }}</td>
<td class="col-2 justify-content-center">{{ RBI }}</td>
</tr>
</transition>
` // End template
})
};
The code provided doesn't actually call emitPlayerSeasonStatsClicked but I assume that's supposed to go on the <td> that includes the name.
If you write the click listener like this:
<td
class="col-4 justify-content-center"
scope="row"
title="Click for Season Stats"
#click="emitPlayerSeasonStatsClicked(playerStats.player.ID)"
>
Then include the id as part of the event emitted by the event bus:
emitPlayerSeasonStatsClicked: function(playerId) {
EventBus.$emit("showPlayerTemplateClicked", playerId);
}
Listening for this in the mounted would be:
mounted: function() {
EventBus.$on("showPlayerTemplateClicked", this.onShowPlayerTemplateClicked);
},
with method:
methods: {
onShowPlayerTemplateClicked: function(playerId) {
if (playerId === this.props_player_id) {
this.showComponent = !this.showComponent;
}
}
}
Assuming the player ids are unique that should be enough to get it working.
However...
The choice of an event bus to pass data to a child seems a poor one. There are several ways this could be done. One way would be to only create it when it's showing (external v-if). Another would be to use props. Yet another would be to use refs to call a method on the child.
I don't understand how your code ever toggled anything. The this value for the listener in mounted will not be the component. I've fixed that by moving it to a method, which Vue will bind correctly. Another reason to move this to a method is that it allows you to remove the listener when the component is destroyed.
v-if:showComponent is not a thing. I assume that should be v-if="showComponent".
Your <tr> elements seem to be immediate children of <div> elements. That isn't correct HTML for tables.

How to pass a component to render in props in Vue Js?

I have a situation where i need to render data cell synamically
where tableProps contain all columns and dataProps.
tableProps: {
cols: [{
cellProps: {
class: "as"
},
cellRenderer: ((data) => {
return <a onlick = {
this.onDataClick
}
class = "btn btn-link" > {
data.name
} < /a>
}).bind(this),
dataKey: "name",
dataType: String,
label: "Name",
sortable: true
}
],
enableSelect: true,
onPageChange: this.onPageChange,
onSelect: (selectedRow) => console.log("selectedRow", selectedRow),
onSelectAll: (data) => console.log("slectAllClick", data),
page: 0,
rowProps: {
onClick: (event, rowData) => {
this.onClick(rowData);
}
},
rowsPerPage: 5,
title: "Nutrition"
}
There is a cell renderer where data can be passed to render custom data like buttons anchor etc..
the solution has been found, instead of sending a function, scoped slots can be used to render dynamic contents for each cell. Thank you for showing interest.
**Table.Vue(child, generic-table)**
<table class="table table-bordered">
<thead>
<tr>
<th v-for="col in options.cols" :key="col.id">
<template v-if="col.colRenderer">
{{col.colRenderer(col)}}
</template>
<template v-else>
{{col.label}}
</template>
</th>
</tr>
</thead>
<tbody>
<tr v-for="datum in data" :key="datum.id" #click="(e)=> options.rowProps.onClick ? options.rowProps.onClick(e, datum): ''">
<td v-for="col in options.cols" :key="col.id" #click="()=> col.onClick ? col.onClick(datum[col.dataKey]): ''">
<template v-if="col.cellSlot">
<slot :name="col.cellSlot.name" :data="datum[col.dataKey]"/>
</template>
<template v-else>
{{datum[col.dataKey]}}
</template>
</td>
</tr>
</tbody>
</table>
**Calling component(Parent, with Custom Data cells)**
<v-table
:name="carePlanName"
:options="tableProps"
:total-count="totalCount"
:data="users" >
<div
slot=""
slot-scope="slotProps">
<!-- Define a custom template for CellData Data -->
<!-- `slotProps` to customize each todo. -->
<span v-if="slotProps">✓
<button>{{ slotProps.name }}</button>
</span>
</div>
</v-table>

Vuejs2- Avoid repetition of select field options using vee-validate

Iam using vee-validate plugin for validation. In my form, there is a select field in the table. Rows will be added dynamically in the table. I don't want to select the same select(Description column) option again and again Image. Hence I want to throw an error like "Selected description already exists in a table" this using vee-validate. Kindly help me to solve this.
Here is my code:
<template>
<div>
<b-card>
<div class="panel-body" id="app">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 20px;">No.</th>
<th style="width: 330px;">Description</th>
<th style="width: 130px;" class="text-right">Charges</th>
<th style="width: 130px;">Total</th>
<th style="width: 130px;"></th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in rows" :key="row.qty">
<td>
{{ index +1 }}
</td>
<td>
<select class="form-control" v-model="row.billChgDesc" v-validate="'required|check'" :name="'billChgDesc' + index" data-vv-as="Description" #change="checkRepetation">
<option v-for="option in billChgDescOpt" v-bind:value="option.value"
:key="option.value"> {{ option.text }}
</option>
</select>
<span v-show=" errors.has('billChgDesc' + index)" class="is-danger">{{ errors.first('billChgDesc' + index) }}</span>
</td>
<td>
<input class="form-control text-right" type="text" v-model="row.charges" data-type="currency" v-validate="'required'" :name="'charges' + index" data-vv-as="Charges" >
<span v-show=" errors.has('charges' + index)" class="is-danger">{{ errors.first('charges' + index) }}</span>
<td>
<input class="form-control text-right" :value="row.qty * row.charges" number readonly />
<input type="hidden" :value="row.qty * row.charges * row.tax / 100" number/>
</td>
<td>
<button class="btn btn-primary btn-sm" #click="addRow(index)"><i class="fa fa-plus"></i></button>
<button class="btn btn-danger btn-sm" #click="removeRow(index)"><i class="fa fa-minus"></i></button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-right">TOTAL</td>
<td colspan="1" class="text-right"><input class="form-control text-right" v-model="delivery" number/></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</b-card>
</div>
</template>
<script>
import Vue from 'vue'
import accounting from 'accounting'
export default {
data: function () {
return {
billChgDescOpt: [
{ value: '', text: 'Select' },
{ value: 'M', text: 'Maintenance Fee'},
{ value: 'W', text: 'Water Charges'},
{ value: 'P', text: 'Penalty Fee'},
],
rows: [
{qty: 5, billChgDesc: '', charges: 55.20, tax: 10},
{qty: 19, billChgDesc: '', charges: 1255.20, tax: 20},
],
grandtotal: 0,
delivery: 40,
selectArr:[]
}
},
methods: {
addRow: function (index) {
try {
this.rows.splice(index + 1, 0, {});
} catch(e)
{
console.log(e);
}
},
removeRow: function (index) {
this.rows.splice(index, 1);
},
checkRepetation:function(){
this.$validator.extend('check', {
getMessage: field => '* Slected ' + field + ' already exists',
validate: function(value){
selectArr.push(value);
}
})
}
}
}
</script>
<style lang="scss" scoped>
.is-danger{
color: RED;
}
</style>
Thanks in advance.
You're on the right track, but a couple changes need to be made. When you call this.$validator.extend, that only needs to be done once - when your component is created. It attaches the check method to the validator, so then every time you have the attribute v-validate="'required|check'" in your HTML, it will run that check method.
In your check validator, you need to answer the question "is this value already selected". The answer is to go through the this.rows and see if any of them have the same billChgDesc property. Because this is in Vue, by the time the validator gets run, the row in question already does have that value, so you want to check if MORE than one row have that value. So, something like this:
mounted() {
var self = this;
this.$validator.extend('check', {
getMessage: field => '* Selected ' + field + ' already exists',
validate: function(value){
return (self.rows.filter(function(v){
return v.billChgDesc == value;
}).length <= 1);
}
});
}
This validator returns true if only one item has the given value. I'm using the built-in filter method of Array (see docs).
You can see an example of this all working here: https://jsfiddle.net/ryleyb/f9q50wx4/1/

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>