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.
Related
Below is the data in a component
data: function () {
return {
sltAreaStyle: {
paddingTop: "3%",
},
checkedTypes: ["krw", "btc", "usdt"],
};
},
Below is watch function of checkedTypes data
watch: {
checkedTypes: {
handler: function (newVal, oldVal) {
if (newVal.length < 1) {
alert("Choose one or more.");
var last = oldVal[0];
this.$data.checkedTypes = [last];
}
},
},
},
Below is my html template
<div class="ckbxArea">
<input type="checkbox" value="krw" v-model="checkedTypes">KRW</input>
<input type="checkbox" value="btc" v-model="checkedTypes">BTC</input>
<input type="checkbox" value="usdt" v-model="checkedTypes">USDT</input>
</div>
I want to change the last value to checkedTypes data when all the check boxes are unchecked.
If the first checkbox was finally unchecked, the checkedTypes would be 'krw' like checkedTypes = ['krw'] The checkedTypes data is ['krw'], but all checkbox tags are unchecked. That is, dom has not been updated. I don't think I understand Vue's life cycle well. I think this problem is related to the life cycle of v-model and components, but I don't know what the problem is. Please explain why this problem occurs and tell me how to solve it.
Well this is more about Vue rendering mechanisms for v-modeleld input controls.
Check this:
Only one last checkbox is checked so model value is ['krw']
Uncheck last checkbox
Watcher is executed - new model value is [] BUT the watcher immediately sets it to same value as before ... ['krw']
Vue re renders the template (see the message in the console) BUT as the v-model value is same as during last render, it does not update the checkbox
Simple solution to situations like this is to postpone the update to next rendering cycle using nextTick
this.$nextTick(() => {
this.checkedTypes = [last];
})
new Vue({
el: "#app",
data: function () {
return {
checkedTypes: ["krw", "btc", "usdt"],
};
},
updated() {
console.log("Component updated")
},
watch: {
checkedTypes: {
handler: function (newVal, oldVal) {
if (newVal.length < 1) {
alert("Choose one or more.");
//console.log("Choose one or more.");
var last = oldVal[0];
// this.checkedTypes = [last];
this.$nextTick(() => {
this.checkedTypes = [last];
})
}
},
},
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.js"></script>
<div id="app">
<input type="checkbox" value="krw" v-model="checkedTypes"/> KRW
<input type="checkbox" value="btc" v-model="checkedTypes"/> BTC
<input type="checkbox" value="usdt" v-model="checkedTypes"/> USDT
<pre>{{ checkedTypes }}</pre>
</div>
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>
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.
I have a <select>-element that has a data property bound to it using v-model in Vue.
Sometimes I want to change that value dynamically. I also have an event-listener attached to this element which is triggered on the change-event. See code example:
<template>
<div class="mySelector">
<select id="testSelect" v-model="mySelectModel"
#change="onChange($event)">
<template v-for="(item, index) in someList">
<option :class="['btn', 'btn-default', 'removing-button']" :value="index">{{item.name}}</option>
</template>
</select>
</div>
</template>
<script>
export default {
data() {
return {
mySelectModel: null
}
},
props: {
},
methods: {
customChange: function() {
this.mySelectModel = ... // some value we from somewhere else that is set dynamically on some condiftion
},
onChange: function (event) {
if (!event) return;
// DO SOMETHING THAT WE ONLY WANT TO DO ON A REAL CLICK
}
},
}
</script>
The problem I have is that when I change the data value mySelectModel dynamically, like in the customChange-method, the change event is also called, triggering the method onChange. I only want to do stuff in that method if it was really triggered by a real click, not when it was changed dynamically.
I can not find a way to distinguish between those cases when the change-event is triggered by a click or when it is just changed for some other reason. Any suggestions?
See vue-js-selected-doesnt-triggering-change-event-select-option, it appears that select does not trigger #change when v-model is updated by JS (only when the selected value is changed by user).
A directive can add the functionality
Vue.directive('binding-change', {
update: function (el, binding, vnode) {
const model = vnode.data.directives.find(d => d.name === 'model')
if (model) {
binding.value(model.value)
}
}
})
use like
<select id="testSelect"
v-binding-change="onChange"
v-model="mySelectModel"
#change="onChange($event)">
Not sure about the parameter to onChange - I'll give it a test.
Similar to this suggested solution, you can make a settable computed that you v-model in your widget:
The get function simply returns the data item
The set function does whatever you want a change in the widget to do, in addition to setting the data item
Other code can change the data item directly and will not execute the set code of the computed.
new Vue({
el: '#app',
data: {
values: ['one','two','three'],
selectedItem: 'two'
},
computed: {
wrappedSelectedItem: {
get() { return this.selectedItem; },
set(value) {
console.log("Changed in widget");
this.selectedItem = value;
}
}
},
methods: {
changeToThree() {
console.log("Stealth change!");
this.selectedItem = 'three';
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<select v-model="wrappedSelectedItem">
<option v-for="value in values" :value="value">{{value}}</option>
</select>
<button #click="changeToThree">Set to three</button>
</div>
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/