Vue js text field value becomes empty when new row is appended - vue.js

I have an dynamic table where new row can be added as well as removed ,im populating text field values based on Onchange event of Select option ,For the first row i can populate the values but once i append a new row ,the old values of the past row gets removed.
export default {
data() {
return {
//editmode:true,
templatename: '',
type: 'margin',
games: {},
fields: {},
subs: {},
rows: []
// errors: {},
}
},
components: {
Datepicker,
MinusIcon,
PlusIcon,
XCircleIcon
},
methods: {
getexcercisesrepcount: function(event, dynamicrow) {
var currentrow = dynamicrow;
var excercise = event;
const URL = baseurl + `api/getexcercisesrepcount/`
axios({
method: 'post',
url: URL,
headers: {
'Content-Type': 'application/json',
},
data: excercise
})
.then(response => {
var rowsdata = response.data;
var i = 0;
for (i; i < rowsdata.length; i++) {
var exid = rowsdata[i].id;
if (dynamicrow != null) {
var crval = $("#set" + currentrow).val(exid); //This is where i set value for the table row
}
}
});
},
addRow: function() {
var elem = document.createElement('tr');
this.rows.push({
workoutname: "",
workoutcategory: "",
set: "",
rep: "",
resttime: "",
tempo: ""
})
},
removeElement: function(index) {
this.rows.splice(index, 1);
},
},
}
This is my table where i have added text fields as well as select option
<table class="border-collapse" style="width: 100%;" id="mytable">
<tr>
<th class="p-2 border border-solid d-theme-border-grey-light" width="25%">Workout</th>
<th class="p-2 border border-solid d-theme-border-grey-light text-center" width="25%">Excercise</th>
<th class="p-2 border border-solid d-theme-border-grey-light">Set</th>
<th class="p-2 border border-solid d-theme-border-grey-light">Rep</th>
<th class="p-2 border border-solid d-theme-border-grey-light">Rest time</th>
<th class="p-2 border border-solid d-theme-border-grey-light">Tempo</th>
</tr>
<tr :id="'tr'+index" v-for="(row, index) in rows" :key="index">
<td class="p-2 border border-solid d-theme-border-grey-light">
<vs-select v-model="row.workoutname" v-validate="'required'" #change="getworkoutexcercises($event)" name="workoutname[]" :id="'wrkout'+index" class="w-full select-large" autocomplete>
<vs-select-item :key="game.id" :value="game.id" :text="game.workoutname" v-for="game in games" class="w-full" />
</vs-select>
</td>
<td class="p-2 border border-solid d-theme-border-grey-light">
<vs-select v-model="row.workoutcategory" v-validate="'required'" #change="getexcercisesrepcount($event,index)" name="workoutcategory[]" :id="'wrkcat'+index" class="w-full select-large" autocomplete>
<vs-select-item :key="game.id" :value="game.id" :text="game.subexname" v-for="game in subs" class="w-full" />
</vs-select>
</td>
<td class="p-2 border border-solid d-theme-border-grey-light"><input type="text" class="form-control" v-validate="'required'" v-model="row.exsets" name="set[]" :id="'set'+index" value="" /></td>
<td class="p-2 border border-solid d-theme-border-grey-light"><input type="text" class="form-control" v-validate="'required'" v-model="row.rep" name="rep[]" :id="'rep'+index" /></td>
<td class="p-2 border border-solid d-theme-border-grey-light"><input type="text" class="form-control" v-validate="'required'" v-model="row.resttime" name="resttime[]" :id="'resttime'+index" /></td>
<td class="p-2 border border-solid d-theme-border-grey-light"><input type="text" class="form-control" v-validate="'required'" v-model="row.tempo" name="tempo[]" :id="'tempo'+index" /></td>
<a v-on:click="removeElement(index);" style="cursor: pointer">
<x-circle-icon size="1.5x" class="custom-class" style="margin-left: 5px;margin-top: 15px;color:red;"></x-circle-icon>
</a>
</tr>
</table>
<button style="margin-top:15px;margin-bottom:15px;" class="button btn-primary" #click="addRow">Add row</button>

You shouldn't create HTML in vue components, rather you should use the virtual DOM and things like v-for to create the HTML you want instead of doing it with createElement.
And in the case that you do use the v-dom and you run into a similar issue, you want to use key to uniquely tag each node so Vue knows what belongs with what.
But main point is that you shouldn't be manipulating the DOM directly from Vue functions ever. It's performance taxing and you lose the benefit of reactivity, as you have noticed.
Look into v-for and vue's documentation on rendering lists for what you are trying to achieve: https://v2.vuejs.org/v2/guide/list.html

Related

Unable to set the value to the first input field using id of the field - in vue

I have an input field and a button (when clicked on displays a dropdown with few items) when selecting the items it has to be shown on the first input field. Similarly when clicking on the 2nd button where the dropdown is shown the selected value is shown in the 2nd input field. This entire runs in a for loop , which is where I am facing the problem.
<tr v-for="items in itemList">
<td valign="top"> {{items}} </td>
<td align="left" nowrap>
<input v-model="itemCode" type="text" :id="'item_code_'+items"
#input="handleInput"
size="20" maxlength="27"
autocomplete="off">
<br/>
</td>
<td align="left" nowrap>
<a id="myDropdown" class="dropdown" style="text-decoration:none;font-
size:10pt; padding-left: 10px;padding-right: 10px;"
#click="loadFavs()"
href="javascript:void(0)" title="Click to choose an item from your
favorites">
<img hspace="3" alt="Favorites" src="/images/icons/LoadFav.png"
height="16" width="16"
onmousemove="this.style.cursor='pointer'"
:id="'bd_fav_image_' + items" title="Click to choose an item from
your favorites">
<select class="dropdown-content" v-if="showFav" name="BOMList"
:id="'bd_list_'+items" style="font-size:10pt;width: 100%" v-
model="selected" #change="selectingFav(items)">
<option value=""></option>
<option v-for="(fav,index) in favList" :id="index" v-
bind:value="fav" :key="fav" v-bind:index="index">{{fav}}
{{index}}</option>
</select>
</a>
</td>
<td valign="top" nowrap >
<input type="Text"
:id="'bd_qty_ '+ index"
value="" size="2"
inputmode="numeric"
maxlength="">
</td>
</tr>
favList--> this list holds a list of items , For eg:- A,B,C,D
When I select A it has to be shown in the input field.
selectingFav: function(value) {
console.log("Inside the selectingFav..." + this.selected + "value is ." +value);
setTheValue(value);
}
function setTheValue(val){
console.log("Inside the setThevlaue");
if (val === 1 ){
console.log("inside the if");
$j('#item_code_1').val(this.selected);
console.log("inside the if witht the value " + $j('#item_code_1').val());
}
Tried setting the value based on the id of the input field but nothing is showing up.
If I set the v-model to the input field then all the 3 fields will be showing up the same value.
Can someone please let me know what is the issue. Hope these details are sufficient.
a) v-model is internally implemented as:
<input v-model="myval">
<!-- is --!>
<input :model-value="myval" #update:model-value="v => myval = v">
so you can freely define your own function
<input v-for="obj, ind of whatever" #update:model-value="v => myfn(v, obj, ind)">
b) same as you have an array you v-for on you may make a parallel array
// you get the idea
data: () => ({ imputs: entries.map(e => 0) })
<div v-for="entry, ind of imputs">
<Whatever :entry="entry"/>
<imput v-model="imputs[ind]">
</div>
c) keep your imputs in objects, generally the best choice
// you get the idea
data: () => ({ imputs: entries.map(e => ({ entry: e, input: 0 })) })
// or
computed: {pairs(){ return this.entries.map(e => ({ entry: e, input: 0 })) }}
<div v-for="item of imputs">
<Whatever :entry="item.entry"/>
<imput v-model="item.input">
</div>
Here is how you can achieve that.
data() {
return {
itemList: [
{ id: 1 , value: '' },
{ id: 2, value: '' },
{ id: 3, value: '' }
]
}
},
methods:{
selectingFav: function(value) {
// value holds the index
if (value === 1 )
this.itemList[0].value = this.selected;
else if(value === 2 )
this.itemList[1].value = this.selected;
else
this.itemList[2].value = this.selected;
}
}
}
In HTML template section
<tr v-for="(items,i) in itemList">
<td valign="top"> {{items.id}} </td>
<td align="left" nowrap>
<input v-model="items.value" type="text" :id="'item_code_'+items"
#input="handleInput" size="20" maxlength="27" autocomplete="off">
<br/>
</td>

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/

How to edit particular row in a table by popping a form modal in vuejs?

I am using a modal form to add new details to the row of a table. After adding details, I’m just adding edit and delete buttons at the end of it. Now here delete button is working fine. How to edit a row of a table by popping replicate of form modal by clicking “edit” button in a row.
Here’s my code:
<div class="layout-padding">
<div
class="item item-link"
v-for="modal in types"
#click="$refs[modal.ref].open()"
>
<i class="item-primary">add</i>
<div class="item-content has-secondary">
<div>{{modal.label}}</div>
</div>
</div>
</div>
<q-modal ref="maximizedModal" class="minimized" :content-css="{padding: '50px'}">
<div class="main">
<label>Enter Your Name:</label>
<input id="name" name="name" type="text" placeholder="Enter your Name" v-model="YourName">
<br>
<label>I am:</label>
<input type="radio" id="Male" value="male" v-model="picked">
Male
<input type="radio" id="Female" value="female" v-model="picked">
Female
<br>
<div class="button">
<button class="red" #click="$refs.maximizedModal.close()">Close</button>
<button class="blue" v-on:click="sub" #click="$refs.maximizedModal.close()">Submit</button>
</div>
</div>
</q-modal>
<table>
<thead>
<th>Name</th>
<th>Gender</th> </thead>
<tbody class="result">
<tr v-for="(h, index) in final">
<td v-for="(value, key, index) in h">
{{ value }}
</td>
<td><button id="edit" class="green edit" v-for="modal in types"
#click="ed(h, index);$refs[modal.ref].open()" type="submit">EDIT</button></td>
<td><button id="delete" class="red delete" v-on:click="del(index)" type="submit">Delete</button></td>
</tr>
</tbody>
</table>
And my script is:
export default {
data () {
return {YourName: '',
details: [],
final: [],
types: [
{
label: 'Add Details',
ref: 'maximizedModal'
}
],
position: 'bottom'
}
},
methods: {
sub: function () {
this.details = {
'name': this.YourName,
'gender': this.picked,
}
this.ed()
if (index === '[object MouseEvent]') {
this.final.push(this.details)
}
if (index > -1) {
this.final.splice(index, 1, this.details)
}
else {
alert('else')
alert(JSON.stringify(this.details))
this.final.push(this.details)
}
},
del: function (index) {
this.$delete(this.final, index)
},
ed: function (details, index) {
return index
}
}
}
If edit button is clicked, the same row should be edited. I don’t know how to proceed further. Please, guide me.
Using the 'splice' can able to modify the given array of object.
Can simply include this inside an 'if' loop:
this.final.splice(this.indi, 1, this.details)