Javascript object data binding with Vue - javascript

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;
}
}
});

Related

Why doesn't an unassigned Vue instance get garbage collected?

Here's the standard way to use VueJS on the HTML page (without bundles). No assignment.
<script>
new Vue({
el: '#root',
data: {
title: 'Hello'
}
});
</script>
Why Garbage Collector doesn't collect this Vue object?
When you instantiate a Vue object, it actually mounts itself to the DOM element, here #root element, as briefly hinted in this documentation page The Vue Instance > Instance Lifecycle Hooks.
By using Developer Tools in your browser, like in Chrome, you can open the console tab and prompt, type console.log(window.$vm0); and hit enter. And you get access to your Vue runtime instance even it was not assigned to a variable:
> Vue {_uid: 2, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
I've opened another question on how to properly access the Vue instance if it wasn't assigned to a variable during instantiation.
The main point, as an answer to this current question, is that there is actually variable assignment / DOM mounting happening behind the scenes by Vue itself, so that is why garbage collection is not triggering.
PS. There is a detailed documentation article Avoiding Memory Leaks in relation to handling Garbage Collection in a Vue application.
A Vue application consists of a Vue instance created with new Vue and mounted in DOM element with id '#root'. Vue is running all this magic behind the scene that's why garbage collector will not collect Vue object.
In addition to data properties, Vue instances expose a number of instance properties and methods. These are prefixed with $ to differentiate them from user-defined properties. For example:
var data = { title: 'Hello' }
var vm = new Vue({
el: '#root',
data: data
});
// If you check below code
vm.$data === data // => true
vm.$el === document.getElementById('root') // => true

Declaring data in Vue with components

According to the docs, this is how you declare data in Vue:
data: {
name: 'Vue.js'
}
However, when I do that it doesn't work and an error shows in the console:
The "data" option should be a function that returns a per-instance value in component definitions.
I change it to the following and then it works fine:
data() {
return {
name: 'Vue.js',
}
}
Why do the Vue docs show the top bit of code when it doesn't work? Is there something wrong on my end?
Edit: This only happens when using components.
In a root Vue instance (which is constructed via new Vue({ . . . }), you can simply use data: { . . . } without any problems.
When you are planing to reuse Vue components using Vue.component(...) or using "template" tag, Use data attribute as a function.
Please review the corresponding section of the Vue.js documentation for more information regarding this problem
https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function
You should declare data in Vue.js by doing
var app = new Vue({
el: '#app', //This is the container in which Vue will be in. The #app means the id of the container is app
data: {
}
});
It turns out you need to declare data in components different than when you set it on a Vue object.
Instead, a component’s data option must be a function, so that each instance can maintain an independent copy of the returned data object:
More: Vue docs

Vue.js update only works if other property changed?

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);
}

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/

Knockout component updating observable not being subscribed to by parent view model?

I've written a component called Upload which allows users to upload files and then report back with a JSON object with these files. In this particular instance, the Upload component has a parameter which comes from a parent view model:
<upload params="dropzoneId: 'uploadFilesDropzone', postLocation: '/create/upload', uploadedFiles: uploadedFiles"></upload>
The one of importance is called uploadedFiles. The parameter binding here means I can reference params.uploadedFiles on my component and .push() new objects onto it as they get uploaded. The data being passed, also called uploadedFiles, is an observableArray on my parent view model:
var UploadViewModel = function () {
// Files ready to be submitted to the queue.
self.uploadedFiles = ko.observableArray([]);
};
I can indeed confirm that on my component, params.uploadedFiles is an observableArray, as it has a push method. After altering this value on the component, I can console.log() it to see that it has actually changed:
params.uploadedFiles.push(object);
console.log(params.uploadedFiles().length); // was 0, now returns 1
The problem is that this change does not seem to be reflected on my parent viewmodel. self.uploadedFiles() does not change and still reports a length of 0.
No matter if I add a self.uploadedFiles.subscribe(function(newValue) {}); subscription in my parent viewmodel.
No matter if I also add a params.uploadedFiles.valueHasMutated() method onto my component after the change.
How can I get the changes from my array on my component to be reflected in the array on my parent view model?
Why do you create a new observable array when the source already is one? You can't expect a new object to have the same reference as another one: simply pass it to your component viewModel as this.uploads = params.uploads. In the below trimmed-down version of your example, you'll see upon clicking the Add button that both arrays (well the same array referenced in different contexts) stay in sync.
ko.components.register('upload', {
viewModel: function(params) {
this.uploads = params.uploads;
this.addUpload = function() { this.uploads.push('item'); }.bind(this);
},
template: [
'<div><button type="button" data-bind="click: addUpload">Add upload</button>',
'<span data-bind="text: uploads().length + \' - \' + $root.uploads().length"></span></div>'].join('')
});
var app = {
uploads: ko.observableArray([])
};
ko.applyBindings(app);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="component: {name: 'upload', params: {uploads: uploads}}"></div>
It is only in case your source array is not observable that things get a little more complicated and you need to have a manual subscription to update the source, eg. you would insert the following in the viewModel:
this.uploads.subscribe(function(newValue) { params.uploads = newValue; });
Additionally the output in the text binding would not be updated for the source because it is not observable. If for some reason that I cannot conceive of you would want to have 2 different observableArrays (1 source & 1 component), you should still be able to do with the line above, but replace the function code with params.uploads(newValue)
The problem may be related to this bug (to be confirmed): https://github.com/knockout/knockout/issues/1863
Edit 1: So this was not a bug. You have to unwrap the raw param to access the original observable. In your case, it would be:
params.$raw.uploadedFiles() //this would give you access to the original observableArray and from there, you can "push", "remove", etc.
The problem is that when you pass a param to a component, it gets wrapped in a computed observable and when you unwrap it, you don't have the original observableArray.
Reference: http://knockoutjs.com/documentation/component-custom-elements.html#advanced-accessing-raw-parameters
While Binding Property that involves Parent --> Child Relation
Use Binding in this way
If You want to bind data to Child Property
data-bind='BindingName : ParentViewmodel.ChildViewModel.ObservableProperty'
Here it seems you want to subscibe to a function when any data is pushed in Array for that you can write subscribe on Length of Observable array which can help you capture event that you want.
This should solve your problem.

Categories

Resources