I am currently faced with a situation where I add options to a select element via vuejs when the #change of that specific element is called.
The problem is that the new option is not 'registered' until I leave the function.
I made a jsfiddle to demonstrate this behaviour: https://jsfiddle.net/bz8361Lp/
In this demo, if a option in the select element is selected, a new option is added to the select. The console is still printing the old number of entries.
new Vue({
el: "#app",
data: {
data: ["test", "hallo", "bye"]
},
methods: {
onChange(event) {
console.log("test")
this.data.push("hello")
const element = document.getElementById("select")
console.log(element.options.length)
}
}
})
I am looking for some guidance on how I can avoid this problem, what I am doing wrong and what a best practice could be.
Thanks.
This is because Vue.js updates DOM not immediately but on the next event loop processing.
You can read about this in detail here
You can check this by using nextTick:
onChange(event) {
console.log("test")
this.data.push("hello")
this.$nextTick(() => {
const element = document.getElementById("select")
console.log(element.options.length)
});
}
need to wait for the dom to render and then evaluate. You can find more information about what the function does '' this.$nextTick()
new Vue({
el: "#app",
data: {
data: ["test", "hallo", "bye"]
},
methods: {
onChange(event) {
console.log("test")
this.data.push("hello")
this.$nextTick(() => {
const element = document.getElementById("select")
console.log(element.options.length)
})
}
}
})
Related
So I have the following data, and my goal is to recalculate the user's results every time data in this object is changed. Here is the data.
data() {
return {
test: 0,
userData: {
sex: null,
age: null,
feet: null,
inches: null,
activity: null,
goal: null,
},
}
}
Now I have tried to implement both watch and computed, but it seams Vue is not noticing when individual items in the object are changed. However, if I take some data out of the object it does notice the change.
Here is what I tried for watch:
watch: {
userData: function () {
console.log("Changed");
}
}
The result was nothing in the console.
For computed I tried the following:
computed: {
results: function () {
console.log(this.userData);
return this.userData.sex;
}
}
But again nothing was printed in the console.
If I tried with the test variable:
watch: {
test: function () {
console.log("Changed");
}
}
It WOULD output changed when the variable was changed. So that works because it is not an object.
Any help would be very appreciated. Again the goal is to recalculate results whenever any userData is changed.
Here is one way to do it. You need (as #match mentioned) use Vue.set() or vm.$set(). I found it was also necessary to update your watcher property to userData.sex.
new Vue({
el: "#app",
data: {
status: '',
userData: {
sex: ''
},
},
methods: {
updateValues(){
// Or Vue.set()
this.$nextTick( function(){
const newUserData = Object.assign({}, this.userData);
newUserData.sex = "Male";
this.userData = newUserData;
});
}
},
watch: {
userData: function (v) {
this.status += "userData Changed";
},
'userData.sex': function (v) {
this.status += "\nuserData.sex Changed";
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.8/dist/vue.min.js"></script>
<div id="app">
<pre v-text="'userData.sex = ' + userData.sex"></pre>
<pre v-text="status"></pre>
<button #click="updateValues()">
Change Sex
</button>
</div>
EDIT:
Re-assigning the whole object, userData, triggers the watch.userData.
Are you actually using the results property (in your template for example)? Computed properties do not get recomputed if they are not being used.
As opposed to what #match says, I doubt you have a reactivity problem since you do not add or delete properties (they already exist in your data so they are already reactive).
I'm trying to write a custom component. And hope I can use it like this
let app = new Vue({
el:'#app',
template:`
<tab>
<tab-item name='1'>
<h1> This is tab item 1</h1>
</tab-item>
<tab-item name='2'>
<h2> This is tab item 2</h2>
</tab-item>
</tab>`,
components:{
tab,
tabItem
}
})
Everything goes fine until you click the button. I got an error from console:
[Vue warn]: You may have an infinite update loop in a component render function.
found in
---> <Tab>
<Root>
I've tried many ways to solve this problem, however, failure always won the debugging competition.
How can I beat this problem?
Here is my code:
let tabItem = {
props:{
name:{
type: String,
required: true
}
},
render(h){
let head = this.$slots.head || ''
let body = this.$slots.default
let tail = this.$slots.tail || ''
return h('div', [
h('div', head),
h('div', body),
h('div', tail)])
}
}
let tab = {
data(){
return {
items:'',
currentView:0
}
},
methods:{
handleTabClick(item){
return ()=>{
let index = this.items.indexOf(item)
this.currentView = this.items[index]
}
},
extractProps(vnode){
return vnode.componentOptions.propsData
}
},
render(h){
this.items = this.$slots.default.filter( node => {
return /tab-item/.test(node.tag)
})
let headers = this.items.map( item => {
let name = this.extractProps(item).name
return h('button', {
on:{
click: this.handleTabClick(item)
}
}, name)
})
let head = h('div', headers)
this.currentView = this.items[0]
return h('div',[head, this.currentView])
}
}
Or any other ways to implement this component?
Thanks a lot for helping me out from the hell.
Thanks for your reply my friends. I'm pretty sure that I get an infinite loop error from the console and my code doesn't work as expected. I don't think using vnode is a good way to implement this component too. However, this is the best solution I can figure out.
This component -- tab should detect its child whose name is tabItem, which is also a component. And tab can extract some data from tabItem. In my case, tab will extract the name property of tabItemn, which will be used to generate the buttons for switching content. Click the button can switch to the relevant content, which is the body of tabItem. In my code, it's currenView.
Like a famous UI library, Element, its tab component can be used like this:
<el-tabs v-model="activeName" #tab-click="handleClick">
<el-tab-pane label="User" name="first">User</el-tab-pane>
<el-tab-pane label="Config" name="second">Config</el-tab-pane>
<el-tab-pane label="Role" name="third">Role</el-tab-pane>
<el-tab-pane label="Task" name="fourth">Task</el-tab-pane>
</el-tabs>
I need to implement one component like this but mine will be more simple. For learning how to do it, I read its source code. Maybe there's not a good way to filter child components. In the source, they use this to filter the el-tab-pane component:
addPanes(item) {
const index = this.$slots.default.filter(item => {
return item.elm.nodeType === 1 && /\bel-tab-pane\b/.test(item.elm.className);
}).indexOf(item.$vnode);
this.panes.splice(index, 0, item);
}
Source Code
I know that I can use $children to access its child components but doing so doesn't guarantee the order of the child components, which is not what I want. Because the order of switching button is important. Detail messages about vnode are not contained in the doc. I need to read the source.
Therefore, after reading the source of Vue, I wrote my code like this then I got my problem.
I finally didn't solve this bug and admit that using this kind of rare code sucks. But I don't know other solutions. So I need you guys help.
Thanks.
You shouldn't change your data in render function, this is wrong
this.items = this.$slots.default.filter( node => {
return /tab-item/.test(node.tag)
})
because it will keep re-rendering, here is a working example for your code, I simply removed items property from data and added new items computed property which returns tab-items nodes.
let tab = {
data(){
return {
currentView:0
}
},
methods:{
handleTabClick(item){
return ()=>{
let index = this.items.indexOf(item)
this.currentView = this.items[index]
}
},
extractProps(vnode){
return vnode.componentOptions.propsData
}
},
computed: {
items(){
return this.$slots.default.filter( node => {
return /tab-item/.test(node.tag)
})
}
},
render(h){
let headers = this.items.map( item => {
let name = this.extractProps(item).name
return h('button', {
on:{
click: this.handleTabClick(item)
}
}, name)
})
let head = h('div', headers)
this.currentView = this.items[0]
return h('div',[head, this.currentView])
}
}
let tabItem = {
name:"tab-item",
props:{
name:{
type: String,
required: true
}
},
render(h){
let head = this.$slots.head || ''
let body = this.$slots.default
let tail = this.$slots.tail || ''
return h('div', [[
h('div', head),
h('div', body),
h('div', tail)]])
}
}
let app = new Vue({
el:'#app',
template:`
<tab>
<tab-item name='1'>
<h1> This is tab item 1</h1>
</tab-item>
<tab-item name='2'>
<h2> This is tab item 2</h2>
</tab-item>
</tab>`,
components:{
tab,
tabItem
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.13/dist/vue.min.js"></script>
<div id="app"></div>
Sorry if its a very easy question, i tried following some answers in here but i couldnt..
I want to add a NEW object to an array, based on the first one
The way i find that works is this one:
new Vue({
el: "#app",
data: {
name: '', //name isnt inside and object
//failedExample: {name: ''}
array: []
},
methods: {
add(){
this.array.push({name: this.name}) // i push a key:value
//this.array.push(failedExample) // what i wished to do
}
}
});
https://jsfiddle.net/myrgato/6mvx0y1a/
I understand that by using the commented array.push, i would just be adding the same reference to the object over and over, so when i change the value of failedExample.name, it will change in all positions of the array. Is there a way that this doesnt happens? Like, i add the first object, then the next one as a NEW instead of a reference?
It should work as you wanted to do with your 'failedExample'. The only wrong thing
I see is that you forget the this keyword when you're pushing into array.
So try this one:
new Vue({
el: "#app",
data: {
failedExample: { name: 'test'},
array: []
},
methods: {
add(){
this.array.push(this.failedExample);
console.log(this.array);
}
}
});
Update: If you want to add a new object each time, then try to clone it so you will not have refference problems:
this.array.push(Object.assign({}, this.failedExample));
I'm having a form in vue js with select drop downs, I'm trying to use https://sagalbot.github.io/vue-select/ this library to execute this, according to the documentation I've to pass an array into this and I'm calling a axios method to get the options of the drop downs. I'm getting the data in following format:
And with following description:
I want to only show the names in the option and get values as ID of that particular row, how can I achieve this.
My code look something like this: https://jsfiddle.net/eemdjLex/1/
<div id="app">
<v-select multiple :options="model.data"></v-select>
</div>
import vSelect from 'vue-select';
Vue.component('v-select', vSelect)
const app = new Vue({
el: '#app'
router: router,
data () {
return {
model: {},
columns: {},
}
}
methods: {
fetchIndexData() {
var vm = this;
axios.get('/companies').then(response => {
Vue.set(vm.$data, 'model', response.data.model)
Vue.set(vm.$data, 'columns', response.data.columns)
}
}
});
It is not working proper but you get the idea what I'm trying to do.
v-select appears to return the entire option as the value when using v-model so I might use a pair of computed values here.
new Vue({
el:"#app",
data:{
serverData,
selected: null
},
computed:{
// serverData is a stand in for your model.data.
// map over that to build your options
selectOptions(){
return this.serverData.map(d => ({label: d.name, value: d.id}))
},
// selectedOption is just a short computed to get the id value
// from whatever option was selected. You could also just use
// "selected.id" in whatever needs the id instead if needed.
selectedOption(){
if (this.selected)
return this.selected.value
else
return null
}
}
})
Example.
Looking at the README on the GitHub page for vue-select, I'm seeing that you can pass the <v-select> component an options property as an array of objects with label and value keys.
I would make a computed property to take your model.data and format it this way:
computed: {
options() {
let data = this.model.data;
let options = [];
// do whatever you need to do to format the data to look like this object:
// options = [{ label: 'foo', value: 1 }, { label: 'bar', value: 2 }]
return options;
}
}
Then pass this computed property to the <v-select> instead:
<v-select multiple :options="options"></v-select>
The official select docs may help you
your v-select component should look like
new Vue({
template: `
<select v-model="selected">
<option v-for="option in options" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
<span>Selected: {{ selected }}</span>`,
el: 'v-select',
props: [ 'options' ]
data: {
selected: ''
}
})
I'm really struggling to get the most basic REST functionality to work in vue.js 2.
I'd like to get data from some endpoint and assign the returned value to a variable of my Vue instance. Here's how far I got.
var link = 'https://jsonplaceholder.typicode.com/users';
var users;
Vue.http.get(link).then(function(response){
users = response.data;
}, function(error){
console.log(error.statusText);
});
new Vue ({
el: '#user-list',
data: {
list: users
}
});
Inside the promise, I can access the response data, but I cannot seem to assign it to users or even to data of the Vue instance.
Needless to say, I'm completely new to vue.js and thankful for any help.
Stack: vue.js 2.03, vue-resource 1.0.3
Cheers!
You can create an object and pass it to the vue instance like that :
var link = 'https://jsonplaceholder.typicode.com/users';
var data = {
list: null
};
Vue.http.get(link).then(function(response){
data.list = response.data;
}, function(error){
console.log(error.statusText);
});
new Vue ({
el: '#app',
data: data
});
Or you can create a function under the methods object and then call it in the mounted function :
var link = 'https://jsonplaceholder.typicode.com/users';
new Vue ({
el: '#app',
data: {
list: null
},
methods:{
getUsers: function(){
this.$http.get(link).then(function(response){
this.list = response.data;
}, function(error){
console.log(error.statusText);
});
}
},
mounted: function () {
this.getUsers();
}
});