Vue: Style list row when selected - vue.js

I have a list of components called client-row and when I select one I want to change the style. I run into an issue when trying to remove the styling from the previously selected row, when selecting a new row.
Vue.component('client-row', {
template: '#client-row',
props: {
client: Object,
},
data: function() {
return {
selected: false
}
},
methods: {
select: function() {
// Does not work properly
el = document.querySelector('.chosen_row')
console.log(el)
if ( el ) {
el.className = el.className - "chosen_row"
}
this.selected = true
this.$emit('selected', this.client)
}
}
})
<script type="text/x-template" id="client-row">
<tr v-bind:class="{ 'chosen_row': selected }">
<td>{{ client.name }}</td>
<td>{{ client.location_name || 'no location found' }}</td>
<td>{{ client.email || 'no email found' }}</td>
<td><button class="btn btn-sm btn-awaken" #click="select()">Select</button></td>
</tr>
</script>
I can properly set the selected property, but cannot seem to remove it reliably.

It is generally bad practice to manually modify DOM elements in components. Instead, I recommend that you change the parent component to have a data field to keep track of which row is selected, and to pass that value into the rows. The row would then check whether its value matches the parent's selected row and apply style if true.
DOM manipulation in components is a sign you are doing things very wrong in vue.
In your case, vue and your manually DOM manipulation are battling each other. Vue is tracking whether to add the chosen_row class on the tr based on whether the child's data field selected is true or not. In your code, you are only ever setting it to true. Vue will always try to include the class for any row you have clicked. Then you are manually removing the class from all rows that were previously clicked, however Vue will still try to add the class because selected is still true in the child components that have been clicked.
You need to do a data oriented approach rather than a DOM manipulation based approach.
Child:
Vue.component('client-row', {
template: '#client-row',
props: {
client: Object,
selectedClient: Object
},
methods: {
select: function() {
this.$emit('selected', this.client);
}
}
})
<script type="text/x-template" id="client-row">
<tr v-bind:class="{ 'chosen_row': client === selectedClient }">
<!-- td's removed for brevity -->
<td><button class="btn btn-sm btn-awaken" #click="select">Select</button></td>
</tr>
</script>
Parent:
Vue.component('parent', {
template: '#parent',
data() {
return {
clients: [],
selectedClient: null
};
},
methods: {
clientSelected(client) {
this.selectedClient = client;
}
}
})
<script type="text/x-template" id="parent">
<!-- i dont know what your parent looks like, so this is as simple as i can make it -->
<div v-for="client in clients">
<client-row :client="client" :selected-client="selectedClient" #selected="clientSelected"></client-row>
</div>
</script>
In addition:
Your click event handler on the button can be shortened to #click="select" which is the recommended way of binding methods.

Related

changing a single value using v-model / full table is redrawn

I was building an editable table, which began to crawl to a halt when the number of rows started to run in the 100's. This led me to investigate what was going on.
In the example below, when changing the value in the input, the whole table is redrawn, and the ifFunction() function is trigged 4 times.
Why is this happening? Shouldn't Vue be capable of just redrawing the respective cell? Have I done something wrong with the key-binding?
<template>
<div id="app">
<table border="1" cellpadding="10">
<tr v-for="(row, rowKey) in locations" :key="`row_+${rowKey}`">
<td v-for="(column, columnKey) in row" :key="`row_+${rowKey}+column_+${columnKey}`">
<span v-if="ifFunction()">{{ column }}</span>
</td>
</tr>
</table>
<input v-model="locations[0][1]">
</div>
</template>
<script>
export default {
data() {
return {
locations: [
["1","John"],
["2","Jake"]
], // TODO : locations is not generic enough.
}
},
methods: {
ifFunction() {
console.log('ifFunction');
return true;
},
}
}
</script>
The data property defines reactive elements - if you change one part of it, everything that's depending on that piece of data will be recalculated.
You can use computed properties to "cache" values, and only update those that really need updating.
I rebuilt your component so computed properties can be used throughout: created a cRow and a cCell component ("custom row" and "custom cell") and built back the table from these components. The row and the cell components each have a computed property that "proxies" the prop to the template - thus also caching it.
On first render you see the ifFunction() four times (this is the number of cells you have based on the data property in Vue instance), but if you change the value with the input field, you only see it once (for every update; you may have to click "Full page" to be able to update the value).
Vue.component('cCell', {
props: {
celldata: {
type: String,
required: true
},
isInput: {
type: Boolean,
required: true
},
coords: {
type: Array,
required: true
}
},
data() {
return {
normalCellData: ''
}
},
watch: {
normalCellData: {
handler: function(value) {
this.$emit('cellinput', {
coords: this.coords,
value
})
},
immediate: false
}
},
template: `<td v-if="ifFunction()"><span v-if="!isInput">{{normalCellData}}</span> <input v-else type="text" v-model="normalCellData" /></td>`,
methods: {
ifFunction() {
console.log('ifFunction');
return true;
},
},
mounted() {
this.normalCellData = this.celldata
}
})
Vue.component('cRow', {
props: {
rowdata: {
type: Array,
required: true
},
rownum: {
type: Number,
required: true
}
},
template: `
<tr>
<td
is="c-cell"
v-for="(item, i) in rowdata"
:celldata="item"
:is-input="!!(i % 2)"
:coords="[i, rownum]"
#cellinput="reemit"
></td>
</tr>`,
methods: {
reemit(data) {
this.$emit('cellinput', data)
}
}
})
new Vue({
el: "#app",
data: {
locations: [
["1", "John"],
["2", "Jake"]
], // TODO : locations is not generic enough.
},
methods: {
updateLocations({
coords,
value
}) {
// creating a copy of the locations data attribute
const loc = JSON.parse(JSON.stringify(this.locations))
loc[coords[1]][coords[0]] = value
// changing the whole locations data attribute to preserve
// reactivity
this.locations = loc
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<table border="1" cellpadding="10">
<tbody>
<tr v-for="(row, i) in locations" is="c-row" :rowdata="row" :rownum="i" #cellinput="updateLocations"></tr>
</tbody>
</table>
<!-- <input v-model="locations[0][1]">
<input v-model="locations[1][1]">-->
{{locations}}
</div>

Vuejs class doesn't update after ajax request

What I'm trying to do is to highlight a table row after the component has been created or mounted. The playingTrack value is being changed to the id of the current song but the class doesn't change.
The #click function works and changes the class to highlight but what I want is for it to happen when component is mounted taking its value from playingTrack variable.
<tr :class="{highlight:track.id == playingTrack}" #click="playingTrack = track.id" v-for="track in tracks">
<td class="align-middle">
{{track.title}} <br> <span style="color: grey;">{{track.artist}}</span>
</td>
</tr>
<script>
export default{
data(){
return{
tracks:{},
album:{},
playingTrack: undefined
}
},
beforeCreate(){
Event.$emit('requestCurrentTrack');
Event.$on('currentTrack', (data) => this.fetchAlbum(data));
},
methods:{
fetchAlbum(data){
axios.get('/api/album/'+this.id).then((response)=>{
if(data){
this.playingTrack = data.id;
}
this.tracks = response.data[0];
this.album = response.data[1][0];
});
}
}
}
</script>
You can use "Computed Property"
https://v2.vuejs.org/v2/guide/computed.html
Here is well-written documentation.

v-for custom component checkbox not working

I am using v-for with a custom component
Vue.component('lineitem', {
template: '#lineitem-template',
props: {
item: {
required: false,
default: null
},
},
computed: {
local_line_item() {
console.log("Re computing line item")
return _.clone(this.item)
},
},
methods: {
set_taxable(e) {
e.preventDefault();
if (this.local_line_item.taxable == false) {
this.local_line_item.taxable = true;
} else {
this.local_line_item.taxable = false;
}
console.log("Changing taxable to ", this.local_line_item);
},
}
});
with this template:
<template id="lineitem-template">
<tr>
<td>
<input type="text" class="form-control" v-model="local_line_item.cost">
</td>
<td>
<div class="option" #click="set_taxable">
<input type="checkbox" v-model="local_line_item.taxable" v-bind:true-value="true" v-bind:false-value="false">
<label for="check1"></label>
</div>
</td>
</tr>
</template>
see this JSFiddle: https://jsfiddle.net/shaunc869/1Ly7mL6n/7/
When I click the checkbox I am changing the value of the internal variable, but the checkbox doesn't change, what am I doing wrong? Thanks!
e.preventDefault() inside set_taxable is the cause of problem. Removing that part works just fine. You can see the updated fiddle here: https://jsfiddle.net/1Ly7mL6n/9/
Also, I don't understand your rationale behind using the click binding to trigger set_taxable. Since you have already used v-model binding to toggle local_line_item.taxable in accordance with the checked state of the checkbox, you do not need a click handler for it. So unless you plan to do some more operations in the click handler, you could remove that as well.

Array Reactivity Issues

I'm having issues with array reactivity, please see this example: https://jsfiddle.net/jk1kadxq/
var app = new Vue({
el: "#app",
data: function () {
return {
grid: {
rows: [{}]
}
}
},
methods: {
addRow: function () {
this.grid.rows.push({});
},
setRow: function (row) {
console.log(row);
this.$set(row, 'cell', 'Test');
}
},
watch: {
'grid.rows': {
deep: true,
handler: function (rows, oldRows) {
console.log('Rows updated', rows, oldRows);
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.js"></script>
<div id="app">
<table>
<tr v-for="row in grid.rows">
<td><input type="text" v-model="row.cell"></td>
<td><button type="button" #click="setRow(row)">Set</button></td>
</tr>
</table>
<button type="button" #click="addRow">Add</button>
</div>
If a row has not been edited manually, clicking "Set" button sets the field to "Test" and all the further updates to it are catched in watcher.
If a row has been edited manually first, watcher is not triggered, and clicking "Set" button does not immediately update the field. Adding another row updates the current row.
Is there a different way to add new array members? This page says it's ok to just add: https://v2.vuejs.org/v2/guide/list.html
I've figured out the issue... All the properties of objects should be initialized.
So in my case, the proper way to add a row is: this.grid.rows.push({cell: ''});
It would be nice though to initialize object properties when binding to controls, if not initialized yet.

How to amend data in a different component?

I am new to Vue js and trying to build a simple CRUD example for myself.
Following the documentation on non parent child communication I would like to amend the heading value in the data of one component but from another component.
I set up a fiddle to showcase the relative functionality as I currently understand it and we have here the HTML:
<div id="app" v-cloak>
<person-add></person-add>
<person-list :list="people"></person-list>
</div>
<template id="person-add-template">
<div>
<h2>
<span>{{ heading }}</span>
Person
</h2>
<form #submit.prevent="handleFormSubmit">
<input type="text" placeholder="Enter persons name" v-model="name" />
<button type="submit" v-show="name">
Add Person
</button>
</form>
</div>
</template>
<template id="person-list-template">
<div>
<h2>People</h2>
<table border="1">
<tr>
<th>Person</th>
<th>Edit</th>
</tr>
<tr v-for="(person, key) in list">
<td>{{ person.name }}</td>
<td><button type="button" #click="editPerson(key)">Edit</button></td>
</tr>
</table>
</div>
</template>
And the JS:
// https://v2.vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication
var bus = new Vue();
// Add
Vue.component('person-add', {
template: '#person-add-template',
props: ['list'],
data: function () {
return {
heading: 'Add',
name: ''
}
},
created: function () {
bus.$on('toggle-heading', function (newHeading) {
console.log(newHeading);
this.heading = newHeading;
});
}
});
// List
Vue.component('person-list', {
template: '#person-list-template',
props: ['list'],
methods: {
editPerson: function (key) {
console.log('fired');
bus.$emit('toggle-heading', 'Edit');
}
}
});
// Vue
new Vue({
el: '#app',
data: {
people: [
{ name: 'Bob' },
{ name: 'Frank' },
{ name: 'Mary' }
]
}
});
As you can see, it presents a simple form that starts with "Add Person" and lists some people along with an edit button for each:
What I would like to happen is that when I click on edit next to a persons name then it will change the heading in the other component to say "Edit Person" as opposed to the default "Add Person".
In the method in component A I have:
editPerson: function (key) {
console.log('fired');
bus.$emit('toggle-heading', 'Edit');
}
And in the created hook within component B I have:
created: function () {
bus.$on('toggle-heading', function (newHeading) {
console.log(newHeading);
this.heading = newHeading;
});
}
When I click edit, in the console I see the logs fired and then Edit so the event seems to follow through to the person-add component but where I have tried to assign the new heading this.heading = newHeading;, the heading does not change and I am battling to understand why.
If anyone could suggest why this is happening, where I have gone wrong with this or how things should be done if this is not the right way then it would be greatly appreciated.
Many thanks in advance!
Your problem is actually to do with scope, not a lack of understanding of Vue. Your code is correct, except you are trying to access this from inside a function creating it's own this context.
Whenever you create a new function this way, it creates it's own this so when you do:
bus.$on('toggle-heading', function(newHeading) {
console.log(newHeading);
// this refers to this anonymous function only
this.heading = newHeading;
});
this only refers to the function itself, not the the Vue instance.
The way to get around this is to use an arrow function, which do not create their own this:
bus.$on('toggle-heading', (newHeading) => {
console.log(newHeading);
// No new 'this' context is created in an arrow function
this.heading = newHeading;
});
Or if you are not using ECMAScript 2015 you will need to set a reference to this outside the function:
var self = this; // set a reference to "this"
bus.$on('toggle-heading', function(newHeading) {
console.log(newHeading);
// Now self refers to the view models `this`
self.heading = newHeading;
});
I've updated your fiddle to show you the two methods:
Arrow Function: https://jsfiddle.net/abtgmx47/3/
Using var self=this reference: https://jsfiddle.net/abtgmx47/4/