Vue.js update only works if other property changed? - javascript

I was wondering what a good approach is for my situation. Consider the following code:
<div id="app-4">
<ol>
<li v-for="todo in todos">
{{ todo.text }} + {{todo.added_text}} <button v-on:click="add_text(todo)">do</button>
</li>
</ol>
</div>
<script type="text/javascript">
var app4 = new Vue({
el: '#app-4',
data: {
todos: [
{ text: 'Learn JavaScript' },
{ text: 'Learn Vue' },
]
},
methods: {
add_text(item){
index = this.todos.indexOf(item);
this.todos[index].added_text = "something else";
// this.todos[index].text = 'learn more';
}
}
})
</script>
I want to add the todo.added_text if the button is pressed. But, because this property was not part of the elements in the array when vue was instantiated, this property does not appear at all (this is my assumption, I could be wrong).
However, when I change something else in the same element (e.g. the commented line: this.todos[index].text = 'learn more';), then both todo.text and todo.added_text update.
My question is, what is a good approach to tell vue that something changed in the element, besides changing some other property of the same element?

But, because this property was not part of the elements in the array when vue was instantiated, this property does not appear at all
Yes you're right
When you pass a plain JavaScript object to a Vue instance as its data option, Vue will walk through all of its properties and convert them to getter/setters using Object.defineProperty.
The getter/setters are invisible to the user, but under the hood they enable Vue to perform dependency-tracking and change-notification when properties are accessed or modified.
Vue cannot detect property addition or deletion. Since Vue performs the getter/setter conversion process during instance initialization, a property must be present in the data object in order for Vue to convert it and make it reactive.
You do this with Vue.set(object, key, value) For example
Vue.set(vm.someObject, 'b', 2)
Read here

You can get reactivity by assigning to a reactive element. this.todos[index] is already reactive, so if you reassign the whole thing, all pieces will be reactive. Because it is an array element, you have to use set, but you can use it once for the whole object rather than for each added member:
add_text(item){
const index = this.todos.indexOf(item);
const newObject = Object.assign({}, item, {
text: 'learn more',
added_text: 'something else'
});
this.$set(this.todos, index, newObject);
}

Related

Vue Components with data and methods

I'm taking an online course which corrects code using a bot. In this course I'm learnig HTML, JS and Vue.JS.
The assignemnt is to create a component, greet that produces <div>Welcome, Alice!</div> when it's called using <greet></greet> The assignment also wants me to use data and methods to make it so it says Welcome, Bob instead when you click om the div element in the component. It tells me to use the data and methods key in the component.
This is my code so far, but I'm stuck.
HTML
<body>
<div id="app"><greet></greet>
</div>
</body>
Vue code:
Vue.component('greet', {
data() {
return {
}
},
template: '<div>Welcome</div>'
})
new Vue({ el: '#app' })
How do I make it so it says Welcome, Alice! and when pressed, it says Welcome, Bob! ?
The bot gives me an output of
file.js
✓ exists
✓ is valid JavaScript
1) renders the correct markup when no clicks are made
2) renders the correct markup when one click is made
You can use the event #click on the div, with that you bind the click event to a specific method in this case I create toggleName()
Vue.component('greet', {
data() {
return {
name: 'Alice'
}
},
methods: {
toggleName() {
this.name = this.name == 'Alice' ? 'Bob' : 'Alice';
}
},
template: '<div #click="toggleName">Welcome {{ name }}</div>'
})
Then we create a method that accesses to the property defined in data in this case is name When we use the reserved word this we access to our instance and from there w can access the property name. Then we create a toggle for the name.
In order to access the variable in our template we need to use the special curly braces
{{ name }} this time the this is not needed.
In your data property, define a variable that will hold access to the name so to speak. In this case, we'll initialize it with a value of Alice:
data () {
return {
name: 'Alice'
}
}
We can add a click method now and bind it to our div element so that it will swap the name:
methods: {
swap () {
this.name = 'Bob'
}
}
Finally, we need to modify our template to receive a click handler and use dynamic bindings to display our name property from our data object:
template: `<div #click="swap">Welcome, <span v-text="name"></span></div>`
Note that in the above illustration the v-text is a directive and we have bound the value of our name property to it, which we modify through the #click handler we assigned to the swap function.

How do I pass an item in a Vuex list to a component?

So, I have a Vuex state that looks like so:
state: {
keylist: ['key1', 'key2', 'key3'],
items: {
key1: {title: "First Item"},
key2: {title: "Second Item"},
key3: {title: "Third Item"}
}
}
And I have a list component, referenced from root like so:
<event-list :list="this.$store.state.keylist"></event-list>
The components is defined like so:
Vue.component('event-list', {
template: "<ul><li v-for='key in list'>{{ key }}</li></ul>",
props: {
list: {
type: Array,
required: true
}
}
})
Now, this all displays the key just fine.
But, of course, what I really want to do is use a component on each item, found by it's key. And that's where I am stuck. I have an item component like so:
Vue.component('event-list-item', {
template: "<h4>{{ item.title }}</h4>",
props: {
item: {
type: Object,
required: true
}
}
})
But I cannot figure out how to translate the key in the parent component into an item in the child component. This template barfs on the first curly brace:
<ul><li v-for='key in list'><event-list-item :item="this.$store.state.items.{{key}}"</li></ul>
And in any case, that doesn't look like the right solution! so What is the right one?
To me the first thing that comes to mind is that's your nested item has no place to be inserted anyway. You might want to have a look at slots for nesting components like that:
https://v2.vuejs.org/v2/guide/components.html#Content-Distribution-with-Slots
Anyway, a few considerations:
In your use case you are better off having the list in the event list item and repeat just the element that you actually need, rather than retrieving it in a useless component that only wraps around another component. I mean:
Vue.component('event-list-item', { template: "<li v-for="item in list"><h4>{{ item.title }}</h4></li>
The store is considered the single source of truth also because it should be easier to access it from the directly impacted components, sparing you from having to hand down multiple props for several layers. Like you are doing. This base is kinda brittle for deeply nested components.
Keep in mind that each component has a different scope and they don't share a thing that's not explicitly passed.
You should access the state items fields like key :
<ul><li v-for='key in list'><event-list-item :item='key' /></li></ul>
and in tht child component :
template: "<h4>{{ store.state.items[item].title }}</h4>"
There is no problem of iterating through properties of items object. It will work the same way as iterating through an array.
<ul>
<li
v-for="item in items"
:key="item.title"
>
<event-list-item :item="item" />
</li>
</ul>
For better practice, you can format data inside a getter, to assign keys and return a list that is ready for rendering so no additional logic is delegated to the component.
Note that key used in code example is for internal use of Vue, as it is a reserved special attribute and is suggested to be present when using v-for loops.

vue JS not propagating changes from parent to component

I am pretty new to Vue Framework. I am trying to propagate the changes from parent to child whenever the attributes are added or removed or, at a later stage, updated outside the component. In the below snippet I am trying to write a component which shows a greeting message based on the name attribute of the node which is passed as property from the parent node.
Everything works fine as expected if the node contains the attribute "name" (in below snippet commented) when initialized. But if the name attribute is added a later stage of execution (here for demonstration purpose i have added a set timeout and applied). The component throws error and the changes are not reflected . I am not sure how I can propagate changes for dynamic attributes in the component which are generated based on other events outside the component.
Basically I wanted to update the component which displays different type of widgets based on server response in dynamic way based on the property passed to it .Whenever the property gets updated I would like the component update itself. Why the two way binding is not working properly in Vuejs?
Vue.component('greeting', {
template: '#treeContainer',
props: {'message':Object},
watch:{
'message': {
handler: function(val) {
console.log('###### changed');
},
deep: true
}
}
});
var data = {
note: 'My Tree',
// name:"Hello World",
children: [
{ name: 'hello' },
{ name: 'wat' }
]
}
function delayedUpdate() {
data.name='Changed World';
console.log(JSON.stringify(data));
}
var vm = new Vue({
el: '#app',
data:{
msg:data
},
method:{ }
});
setTimeout(function(){ delayedUpdate() ;}, 1000)
<script src="https://vuejs.org/js/vue.js"></script>
<div id="app">
<greeting :message="msg"></greeting>
</div>
<script type="text/x-template" id="treeContainer">
<h1>{{message.name}}</h1>
</script>
Edit 1: #Craig's answer helps me to propagate changes based on the attribute name and by calling set on each of the attribute. But what if the data was complex and the greeting was based on many attributes of the node. Here in the example I have gone through a simple use case, but in real world the widget is based on many attributes dynamically sent from the server and each widget attributes differs based on the type of widget. like "Welcome, {{message.name}} . Temperature at {{ message.location }} is {{ message.temp}} . " and so on. Since the attributes of the node differs , is there any way we can update complete tree without traversing through the entire tree in our javascript code and call set on each attribute .Is there anything in VUE framework which can take care of this ?
Vue cannot detect property addition or deletion unless you use the set method (see: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats), so you need to do:
Vue.set(data, 'name', 'changed world')
Here's the JSFiddle: https://jsfiddle.net/f7ae2364/
EDIT
In your case, I think you are going to have to abandon watching the prop and instead go for an event bus if you want to avoid traversing your data. So, first you set up a global bus for your component to listen on:
var bus = new Vue({});
Then when you receive new data you $emit the event onto the bus with the updated data:
bus.$emit('data-updated', data);
And listen for that event inside your component (which can be placed inside the created hook), update the message and force vue to re-render the component (I'm using ES6 here):
created(){
bus.$on('data-updated', (message) => {
this.message = message;
this.$forceUpdate();
})
}
Here's the JSFiddle: https://jsfiddle.net/9trhcjp4/

Angular: Update one-time binded data

Suppose you have in your directive controller an array like
this.items = [
{active: true, title: 'bar', ...},
{active: false, title: 'foo', ...},
...
];
And in the template you visualise it
<ol>
<li ng-repeat="item in ::items" ng-class="...">{{item.title}}</li>
</ol>
Note the :: which makes sure that it does this only once, because the directive injects some DOM elements too after ng-repeat is ready.
DEMO
However, at some point in time the app receives an update
this.items = updatedItems;
The update is identical to the original data except for some of the properties of each item (active might change from true to false for example). Now, the update will not do anything because of the ::. Now I can iterate through every item
updatedItems.forEach((item, index) => {
$scope.items[index].active = item.active;
$scope.items[index].title = item.title;
...
});
So the question is what would be the best approach to this problem ?
How about having a copy of the dataset and data binding that, instead of the actual dataset, changes to which you want the template to ignore, all be it for a particular condition.
Then when you would want the data to be updated you can just update the necessary changes to the copied data and the changes will be reflected. I understand, that does add the overhead of having a copy of the data but a simple solution can often save from a lot of head scratching later, especially if the source is being looked at from a different member of the team.

Javascript object data binding with Vue

I have a JavaScript object that I am attempting to bind to a Vue view.
I am running a function to update the JavaScript object using AJAX and I was expecting Vue to bind to the JS object and update the view when the object is updated though that isn't happening.
Research suggests making the AJAX call within the Vue declaration but due other constraits I would rather not do that.
I've created a fiddle to illustrate what the issue is since it's reproducable without the AJAX portion as well as pasted the code below.
https://jsfiddle.net/g6u2tph7/5/
Thanks in advance for your time and wisdom.
Thanks,
vmitchell85
JavaScript
window.changeTheData = function (){
externalJSSystems = [{description: 'Baz'}, {description: 'Car'}];
document.getElementById("log").innerHTML = 'function has ran...';
// This doesn't update the Vue data
}
var externalJSSystems = [{description: 'Foo'}, {description: 'Bar'}];
Vue.component('systable', {
template: '#sysTable-template',
data() {
return {
systems: externalJSSystems
};
}
});
new Vue({
el: 'body'
});
HTML
<systable :systems="systems"></systable>
<button type="button" onclick="changeTheData()">Change</button>
<br><br>
<div id="log"></div>
<template id="sysTable-template">
<table>
<thead>
<tr>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr v-for="sys in systems">
<td>{{ sys.description }}</td>
</tr>
</tbody>
</table>
</template>
Try this out :
externalJSSystems.push({description: 'Baz'}, {description: 'Car'});
It will append the new objects to externalJSSystems and the view will be updated. Why doesn't your example work ? Because you are assigning a new Array reference to externalJSSystems but Vue is still watching the old one.
To achieve what you want, don't assign a new Array instance but clear it. For example :
window.changeTheData = function (){
externalJSSystems.length = 0
externalJSSystems.push({description: 'Baz'}, {description: 'Car'});
}
When that instance of the systable Component is instantiated, Vue adds an "Observer" class to the initial externalJSSystems Array — extending the Array's prototype, adding getter/setters for each of the properties, and maintaining the two-way binding between the Component's data and the original Array. The changeTheData() method is overwriting that Vue-modified externalJSSystems Array with a completely new Array (that lacks the Observer), thus breaking the two-way binding.
In this way, externalJSSystems.push( … ) works because the default Array methods ('push', 'pop', 'shift', 'unshift', 'splice', 'sort', and 'reverse') have been mutated such that they are handled by the Observer.
I think the key to the behavior you're looking for lies in the Vue Component "props" — http://vuejs.org/guide/components.html#Props. In fact, it looks like your component markup — <systable :systems="systems"></systable> — is already set up to pass dynamic data to the Component instance. Right now, that :systems="systems" isn't doing anything. By defining systems in the Parent Vue scope, and defining systems as a prop(s) within the Component registration, you can pass dynamic data to Components within that Parent's scope.
Component
Vue.component('systable', {
template: '#sysTable-template',
props: {
systems: Array
}
});
Vue Instance
var vm = new Vue({
el: 'body',
data: {
systems: externalJSSystems
}
});
You can see it in action in this fiddle: https://jsfiddle.net/itopizarro/ycr12dgw/
I cached the Vue instance — var vm = new Vue({ … }) — so the changeTheData method had access to its systems data. This gives your external changeTheData() method a reference to the Vue instance where you defined system — thus giving it access to modify (without replacing, or iteratively adding/removing items from…) the Array of data.
Rather than making systems a data property, you can make it a computed property. Like the other answer said, the reference is to the old object. But if you make systems a computed property, it will automatically watch any variable used in the calculation (like externalJSSystems) and re-calculate the computed property.
Vue.component('systable', {
template: '#sysTable-template',
computed: {
systems() {
return externalJSSystems;
}
}
});

Categories

Resources