vue.js data not updated after clear - javascript

I have a vue.js item in my page that tracks changes made to a form. It looks like this:
var changes_applied = [];
var changes_applied_block = new Vue({
name: "ChangesApplied",
el: '#changes-applied',
data: {
items: changes_applied
},
methods: {
remove: function(index) {
changes_applied.splice(index, 1);
}
}
});
When a change is detected the change is pushed onto the changes_applied array, and it shows up in the "Changes Applied" div as expected. The deletes also work, which just calls the remove method on the vue object.
I also have a "clear" button that's not connected to the vue instance, and when it's clicked it sets the data source back to an empty array using changes_applied = [];
The problem is that after this is cleared using the button, the changes / additions to the changes array no longer show up in the vue element-- it's like the vue element is no longer attached to the changes_applied array.
Am I missing a binding or something here that needs to happen, or is there a "vue way" to clear the vue data without touching the actual source array?

You shouldn't be changing the changes_applied array; Vue isn't really reacting to changes on that array. It only sort of works when this.items is pointed to the same array reference. When you change that reference by reassigning changes_applied it breaks because you are then manipulating changes_applied but it is not longer the same array as this.items.
You should instead be manipulating this.items directly:
methods: {
remove: function(index) {
this.items.splice(index, 1);
}
To clear it you can set:
this.items = []
and it will work as expected.

Your items array is initialized with changes_applied but does not mantaing bindings, it's just the default value for items when the instance is created. So if you change the changes_applied this will not affect the items array on vue instance.
example
new Vue({
el: '#app',
data: function () {
return {
items: myArr,
newItem: ''
}
},
methods: {
addItem () {
this.items.push(this.newItem)
this.newItem = ''
},
remove (index) {
this.items.splice(index, 1)
},
clear () {
this.items = []
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<div id="app">
<input type="text" v-model="newItem" />
<button #click="addItem">Add</button>
<button #click="clear">Clear</button>
<p v-for="(item, index) in items" #click="remove(index)">{{item}}</p>
</div>
<!-- from outside vue instance-->
<button onClick="clearFromOutside()">clear from outside</button>
<script>
var myArr = ['hola', 'mundo'];
function clearFromOutside() {
console.log(myArr)
myArr = [];
console.log(myArr)
}
</script>

Mark_M already provided a good explanation, I'll add a demo, since I think its easier to understand how it works.
You can copy the value of the array to data, but then all operations must be done to the data directly:
const changes_applied = [
{id: 1},
{id: 2},
{id: 3}
];
const vm = new Vue({
el: '#app',
data: {items: changes_applied},
methods: {
add() {
const id = this.items.length + 1
this.items.push({id})
},
remove() {
this.items.pop()
},
clear() {
this.items = []
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<div id="app">
<div>
<button type="button" #click="add">Add</button>
<button type="button" #click="remove">Remove</button>
<button type="button" #click="clear">Clear</button>
</div>
<ul name="list">
<li v-for="item in items" :key="item.id">
Item {{ item.id }}
</li>
</ul>
</div>

Related

View isnt updated when a new variable that is added in mounted is changed

I have a list in utils that I use it almost everywhere and it only has text variable in each object.
In one of my components, I need to add done variable to each item in that list so I can toggle them. I can see that the variable is added, but whenever I toggle it, the view does not get updated.
const arrayFromUtils = [{
text: "Learn JavaScript"
},
{
text: "Learn Vue"
},
{
text: "Play around in JSFiddle"
},
{
text: "Build something awesome"
}
];
new Vue({
el: "#app",
data: {
todos: arrayFromUtils
},
mounted() {
this.todos.forEach(item => (item.done = false));
},
methods: {
toggle: function(todo) {
todo.done = !todo.done
console.log('toggled item: ', todo);
}
}
})
.red {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="todo in todos" :key="todo.text">
<div #click="toggle(todo)" :class="{ red: todo.done }">
{{ todo.text }}
</div>
</div>
</div>
I can see that done variable gets updated when toggling an item, but the view does not get updated. What am I doing wrong?
You are adding a property to each of your todos items. To make it reactive, you need to use this.$set. Take a look at the documention about changes detection caveats.
Your mounted function should be:
mounted() {
this.todos.forEach(item => this.$set(item, "done", false));
}
Hey it's problem with Vue Reactivity
https://v2.vuejs.org/v2/guide/reactivity.html
You are changing item in array if you want to do this you need to use vm.$set or map whole array.
If you do this in your way vue can't detect if something changed that will be posible in vue3 :)
mounted() {
this.todos.map(item => ({...item, done: false});
}

What's the proper way to update a child component in the parent's array in Vue?

I'm new to Vue and was hoping for some clarification on best practices.
I'm building an app that uses an array to keep a list of child components and I want to be able to update and remove components by emiting to the parent. To accomplish this I currently have the child check the parent array to find it's index with an "equals" method so that it can pass that index to the parent. This works fine for something simple but if my child components get more complex, there will be more and more data points I'll have to check to make sure I'm changing the correct one. Another way to do this that I can think of is to give the child component an ID prop when it's made and just pass that but then I'd have to handle making sure all the ids are different.
Am I on the right track or is there a better more widely accepted way to do this? I've also tried using indexOf(this._props) to get the index but that doesn't seem to work. Maybe I'm just doing something wrong?
Here's a simplified version of what I'm doing:
// fake localStorage for snippet sandbox
const localStorage = {}
Vue.component('child', {
template: '#template',
data() {
return {
newName: this.name
}
},
props: {
name: String
},
mounted() {
this.newName = this.name
},
methods: {
update() {
this.$emit(
"update-child",
this.$parent.children.findIndex(this.equals),
this.newName
)
},
equals(a) {
return a.name == this.name
}
}
})
var app = new Vue({
el: '#app',
data: {
children: []
},
methods: {
addNewChild() {
this.children.push({
name: 'New Child',
})
},
updateChild(index, newName) {
this.children[index].name = newName
}
},
mounted() {
if (localStorage.children) {
this.children = JSON.parse(localStorage.children)
}
},
watch: {
children(newChildren) {
localStorage.children = JSON.stringify(newChildren)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
<div id="app">
<button v-on:click="addNewChild">+ New Child</button>
<hr />
<child v-for="child in children"
:key="children.indexOf(child)"
:name="child.name"
#update-child="updateChild">
</child>
</div>
<script type="text/x-template" id="template">
<div>
<p><b>Name: {{name}}</b></p>
<input placeholder="Name" type="text" v-model="newName" />
<button #click="update">Update</button>
<hr />
</div>
</script>
The great thing about v-for is that it creates its own scope. With that in mind, you can safely reference child in the event handler. For example
<child
v-for="(child, index) in children"
:key="index"
:name="child.name"
#update-child="updateChild(child, $event)"
/>
updateChild(child, newName) {
child.name = newName
}
All you need to emit from your child component is the new name which will be presented as the event payload $event
update() {
this.$emit("update-child", this.newName)
}
A quick note about :key... it would definitely be better to key on some unique property of the child object (like an id as you suggested).
Keying on array indices is fine if your array only changes in size but if you ever decide to splice or sort it, Vue won't be able to react to that change correctly since the indices never change.

How to re-render an HTML element after a value deep in an multi-dimension array changes?

I have a v-btn whose content is determined by values in the deepest layer of a multi-dimension array.
<div v-for="student in students">
<v-btn disabled v-for="tag in student.tags">{{tag}}</v-btn>
</div>
Here tags is a sub-array.
I want to re-render this button after the values change, but don't know how.
I have already used Vue.set like:
// "this.students" is an array, and "this.students[index].tags" is a sub-array.
// I increment the length of the sub-array.
this.students[index].tags.length++;
// Then add a new value into the sub-array at the end.
Vue.set(this.students[index].tags, this.students[index].tags.length - 1, value)
By printing out to the console, I can see both the values and the length of the sub-array, this.students[index].tags, change, and there should be a new button appear because I added a new value into this.students[index].tags, but there is not. And only after I re-compile the client end, the new button show up.
Could anyone teach how to re-render that button?
Thanks in advance!
Vue only observes the object's own properties - that is, only 1 level deep, no more. So you can try one of these:
use this.$forceUpdate(); (https://v2.vuejs.org/v2/api/#vm-forceUpdate)
use this.$set(this.students[index], 'tags', this.students[index].tags.concat([value])); - once set, the tags array will be observed by Vue so you can use tags.push() on subsequent additions
use a hash-map for students' tags
computed:
{
studentTags()
{
const result = {};
this.students.forEach(student =>
{
result[student.id] = student.tags;
});
return result;
}
},
methods:
{
addTag(studentId, tag)
{
this.studentTags[studentId].push(tag);
}
}
We do not need to use Vue.set to push the new data in an array or sub-array. It will auto-handle by the vuejs.
However, we should use Set to reflect the updates in a sub-array.
See this example-
<template>
<div id="app">
<div v-for="(student, index) in students" :key="index">
<button #click="addMoreTag(student.id)" style="background: green">
Add more
</button>
<button
v-for="(tag, tindex) in student.tags"
:key="tindex + 't'"
#click="updateTag(student.id, tag)"
>
{{ tag }}
</button>
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
students: [
{
id: 1,
name: "John",
tags: ["one", "two"],
},
{
id: 2,
name: "Mira",
tags: ["three", "five"],
},
],
};
},
methods: {
addMoreTag(id) {
let index = this.students.findIndex((item) => item.id === id);
this.students[index].tags.push("new");
},
updateTag(student_id, tag) {
let index = this.students.findIndex((item) => item.id === student_id);
let tagIndex = this.students[index].tags.findIndex(
(item) => item === tag
);
this.$set(this.students[index].tags, tagIndex, "updated");
},
},
};
I wrote in coodepen too- https://codesandbox.io/s/blissful-cdn-5kyt1c?file=/src/App.vue

Why is v-model inside a component inside a v-for (using a computed list) changing weirdly when the list changes?

I'm using a computed list to display several forms for changing comments in a database. (backend Symfony / api requests via axios, but unrelated)
The form for the comments itself is in a Vue component.
The computed list is based on a list that gets loaded (and set as data property) when the page is mounted which is then filtered by an input search box in the computed property.
Now when i type different things in the input box and the comment component gets updated the v-model and labels are messing up.
I've tested in several browsers and the behaviour is the same in the major browsers.
I've also searched the docs and haven't found a solution.
Example to reproduce behaviour:
<!DOCTYPE html>
<html>
<div id="app"></app>
</html>
const ChangeCommentForm = {
name: 'ChangeCommentForm',
props: ['comment', 'id'],
data() {
return {
c: this.comment,
disabled: false
};
},
template: `
<form>
<div>{{ comment }}</div>
<input :disabled="disabled" type="text" v-model="c">
<button type="submit" #click.prevent="changeComment">
Change my comment
</button>
</form>
`,
methods: {
changeComment() {
this.disabled = true;
// do the actual api request (should be unrelated)
// await api.changeCommentOfFruit(this.id, this.c),
// replacing this with a timeout for this example
window.setTimeout(() => this.disabled = false, 1000);
}
}
};
const App = {
components: {ChangeCommentForm},
data() {
return {
fruits: [
{id: 1, text: "apple"},
{id: 2, text: "banana"},
{id: 3, text: "peach"},
{id: 4, text: "blueberry"},
{id: 5, text: "blackberry"},
{id: 6, text: "mango"},
{id: 7, text: "watermelon"},
],
search: ''
}
},
computed: {
fruitsFiltered() {
if (!this.search || this.search === "")
return this.fruits;
const r = [];
for (const v of this.fruits)
if (v.text.includes(this.search))
r.push(v);
return r;
}
},
template: `
<div>
<form><input type="search" v-model="search"></form>
<div v-for="s in fruitsFiltered">
<ChangeCommentForm :id="s.id" :comment="s.text"/>
</div>
</div>
`
};
const vue = new Vue({
el: '#app',
components: {App},
template: '<app/>'
});
Just type some letters in the search box
Example on codepen: https://codepen.io/anon/pen/KLLYmq
Now as shown in the example the div in CommentChangeForm gets updated correctly, but the v-model is broken.
I am wondering if i miss something or this is a bug in Vue?
In order to preserve state of DOM elements between renderings, it's important that v-for elements also have a key attribute. This key should remain consistent between renderings.
Here it looks like the following might do the trick:
<div v-for="s in fruitsFiltered" :key="s.id">
<ChangeCommentForm :id="s.id" :comment="s.text"/>
</div>
See:
https://v2.vuejs.org/v2/guide/list.html#Maintaining-State
https://v2.vuejs.org/v2/api/#key

how to share data between components in VUE js (while creating list)

Could you please tell me how to share data between components in VUE js (while creating list).I have two components list components and add todo component.I want to add items in list when user click on add button.But issue is input field present in different component and list is present in different component
here is my code
https://plnkr.co/edit/bjsVWU6lrWdp2a2CjamQ?p=preview
// Code goes here
var MyComponent = Vue.extend({
template: '#todo-template',
props: ['items']
});
var AddTODO = Vue.extend({
template: '#add-todo',
props: ['m'],
data: function () {
return {
message: ''
}
},
methods: {
addTodo: function () {
console.log(this.message)
console.log(this.m);
//this.m =this.message;
},
},
});
Vue.component('my-component', MyComponent);
Vue.component('add-todo', AddTODO)
var app = new Vue({
el: '#App',
data: {
message: '',
items: []
},
});
The whole point of having a great MVVM framework is to let you have a view-model: a central store of all the state in your page/app/whatever. Components can emit events. You can have an event bus. But if you can save the day with a simple, global variable containing all your state, this is by far the cleanest, best solution. So just put your to-dos in an array, in a variable in global scope, then declare them in the data of every component that needs them. Here it is working in Plunkr.
markup
<div id="App" >
<add-todo></add-todo>
<my-component></my-component>
</div>
<template id="add-todo">
<div>
<input type="text" v-model="message">
<button #click="addTodo">Add todo</button>
</div>
</template>
<template id="todo-template">
<div>
<ul >
<li v-for="(item,index) in store.items">
{{item.message}}
</li>
</ul>
</div>
</template>
<script src="vue.js"></script>
<script src="script.js"></script>
code
// This is the magic store. This is all you need.
var vueStore = {items : []};
var MyComponent = Vue.extend({
template: '#todo-template',
data : function(){return {store : vueStore}}
});
var AddTODO = Vue.extend({
template: '#add-todo',
data: function () {
return {
message: '',
store : vueStore
}
},
methods: {
addTodo: function (event) {
this.store.items.push({'message' : this.message})
},
},
});
Vue.component('my-component', MyComponent);
Vue.component('add-todo', AddTODO)
var app = new Vue({
el: '#App',
data: {
store : vueStore
},
});
This is not a savage hack! We're being called to stop thinking about events, move up the food chain, and think about reactive pipes. Components don't care when or by who the central store gets updated. Vue takes care of it.
Here's the page on state management.
So you could use events and emit the created todo to the root vue instance.
I edited / forked your plunkr (I'm rather the fiddle type).
https://plnkr.co/edit/bnMiDmi30vsj3a8uROBK?p=preview
So I edited this line here, which listens for a custom event added and pushes the first argument to items.
<add-todo v-on:added='items.push(arguments[0])'></add-todo>
And also these lines, which emit the event. And i changed from the property m to the data message, because you shouldnt mutate props:
<input type="text" v-model="message">
<button #click="$emit('added', message)">Add todo</button>

Categories

Resources