Deep watch in not working on object Vue - javascript

I have a watcher setup on an array and I have deep watch enabled on it, however the handler function does not trigger when the array changes, applications is defined in the object returned in data. Here's the code:
watch: {
applications: {
handler: function(val, oldVal) {
console.log('app changed');
},
deep: true,
},
page(newPage) {
console.log('Newpage', newPage);
},
},

Vue cannot detect some changes to an array such as when you directly set an item within the index:
e.g. arr[indexOfItem] = newValue
Here are some alternative ways to detect changes in an array:
Vue.set(arr, indexOfItem, newValue)
or
arr.splice(indexOfItem, 1, newValue)
You can find better understanding of Array Change Detection here

If you reset your array with arr[ index ] = 'some value', Vue doesn't track to this variable. It would better to use Vue array’s mutation method. These methods used to track array change detection by Vue.
It is worked for me.

Related

Vue.js non reactive objects of array

I have one problem. I have
data: {
tracks: []
}
And tracks array will contain a complex object. And when I assign a new value to tracks nested object to become reactivity. But I just don't need not deep reactivity object. How can I do it without created function or JSON.parse?
Because tracks used with Cesium framework and use Vue getter. And FPS becomes 10-15. Without Vue have 50-60 FPS
Use Object.freeze or Object.defineProperty (you only need configurable: false) to prevent Vue from picking up reactivity on large datasets
https://forum.vuejs.org/t/cesium-and-vue-js-data-getters/26928
You can implement a deep watcher on tracks:
watch: {
tracks: {
handler (newVal, oldVal) {
// implement what you want to do here
// If you just wanted to force a re-render you can do:
this.$forceUpdate()
}
deep: true,
}

Vue Watch not triggering

Trying to use vue watch methods but it doesn't seem to trigger for some objects even with deep:true.
In my component, I recieve an array as a prop that are the fields to create
the following forms.
I can build the forms and dynamicly bind them to an object called crudModelCreate and everything works fine (i see in vue dev tools and even submiting the form works according to plan)
But I have a problem trying to watch the changes in that dynamic object.
<md-input v-for="(field, rowIndex) in fields" :key="field.id" v-model="crudModelCreate[field.name]" maxlength="250"></md-input>
...
data() {
return {
state: 1, // This gets changed somewhere in the middle and changes fine
crudModelCreate: {},
}
},
...
watch: {
'state': {
handler: function(val, oldVal) {
this.$emit("changedState", this.state);
// this works fine
},
},
'crudModelCreate': {
handler: function(val, oldVal) {
console.log("beep1")
this.$emit("updatedCreate", this.crudModelCreate);
// This doesn't work
},
deep: true,
immediate: true
},
}
From the docs
Due to the limitations of modern JavaScript (and the abandonment of Object.observe), 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.
Please take a look to Reactivity in Depth https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
In certain circumstances it is possible to force a refresh by adding a key property to the child component containing a json string of the object being passed to it in v-model.
<Component v-model="deepObject" :key="JSON.stringify(deepObject)" />

Adding an item to array does not re-render polymer template

I am trying to render a dynamic list of items using a template of dom-repeat like this:
<template is="dom-repeat" items={{numbers}} as="anumber" >
<div>
{{anumber}}
<paper-button class="deleteThisNumber" index={{index}}></paper-button>
</div>
</template>
<paper-button id="addNumber"></paper-button>
Each item has a button which will delete this item.
There is also a button outside of the dom-repeat template that tries to add an entry to array numbers. The JS looks like this:
Polymer ({
is: "something",
properties: {
numbers: {
type: Array,
value: ["1"]
}
},
removeByIndex: function (array, index) {
return array.filter(function (elem, _index) {
return index != _index;
});
},
attached: function () {
var myself = this;
$(this).on('click', '.deleteThisNumber', {}, function (e) {
myself.numbers = myself.removeByIndex(myself.numbers, this.index)
});
this.$.addNumber.addEventListener("click", function (e) {
myself.numbers.push("123");
})
},
...
});
The result is: deleting works, but adding does not.
By saying "works", I mean the list reflects the change by adding/removing an entry in the DOM. I checked the property numbers it is correctly modified all the time. So why does Polymer not reflect changes of an array property to a template if the change is addition(array.push)? How should I fix this? (I am open to any suggestions other than manually adding divs.)
My Polymer version is 1.X
Change the code for array push to :
this.$.addNumber.addEventListener("click", function(e) {
myself.push("numbers", "123");
})
There has to be an observable change in order to render the updated property or subproperty. An observable change is a data change that Polymer can associate with a path.
If you manipulate an array using the native methods (like Array.prototype.push), you must notify Polymer after the fact. OR, use the Polymer methods for array mutations.
When modifying arrays, a set of array mutation methods are provided on
Polymer element prototypes which mimic Array.prototype methods, with
the exception that they take a path string as the first argument. The
path argument identifies an array on the element to mutate, with the
following arguments matching those of the native Array methods.
These methods perform the mutation action on the array, and then
notify other elements that may be bound to the same array of the
changes. You must use these methods when mutating an array to ensure
that any elements watching the array (via observers, computed
properties, or data bindings) are kept in sync.
Every Polymer element has the following array mutation methods
available:
push(path, item1, [..., itemN])
pop(path)
unshift(path, item1, [...,
itemN])
shift(path)
splice(path, index, removeCount, [item1, ..., itemN])
Learn More
I found out the solution is to force a notifyPath with a little more:
myself.numbers.push("123"); //before only has this
myself.notifyPath('numbers', myself.numbers.slice()); //added
Referred to https://github.com/Polymer/polymer/issues/2068#issuecomment-120767748
Answer from #miyconst
More than one way you can fix your code.
1) The way you've already been using when you delete an item.
this.$.addNumber.addEventListener("click", function (e) {
myself.numbers.push("123");
myself.numbers = myself.numbers.slice();
})
2) Answer from yourself (with slight changes)
this.$.addNumber.addEventListener("click", function (e) {
myself.numbers.push("123");
myself.notifyPath("numbers");
})
3) Answer from #Ofisora
this.$.addNumber.addEventListener("click", function(e) {
myself.push("numbers", "123");
})
Here's why the fixes work, https://www.polymer-project.org/1.0/docs/devguide/data-system#observable-changes

Data binding on JavaScript HTMLElement property in Polymer

Making an SPA using Polymer, and I need my custom components to all use a common custom component which represents my backend API and is responsible of GET-ting/POST-ing data from/to the API. It also serves as a "cache" and holds the data to display. This way, all the components that have access to this single element will share the same data.
So what I want to do is this... :
<my-api
users="{{users}}"
products="{{products}}">
</my-api>
...but programmatically, as <my-api> is not declared in all of my components but once in the top one and then passed down through the hierachy by JavaScript:
Polymer({
is: 'my-component',
properties: {
api: {
observer: '_onApiChanged',
type: HTMLElement
},
products: {
type: Array
},
users: {
type: Array
}
},
_onApiChanged: function(newVal, oldVal) {
if (oldVal)
oldVal.removeEventListener('users-changed', this._onDataChanged);
// Listen for data changes
newVal.addEventListener('users-changed', this._onDataChanged);
// Forward API object to children
this.$.child1.api = newVal;
this.$.child2.api = newVal;
...
},
_onDataChanged: function() {
this.users = this.api.users; // DOESN'T WORK as 'this' === <my-api>
this.products = this.api.products; // Plus I'd have to repeat for every array
}
});
Does Polymer offers a built-in way to do this ? Can I create a double curly braces binding programmatically ?
I would likely architect this slightly differently: passing down the products/users arrays declaratively taking advantage of Polymer's binding system. Or you could write your my-api element in such a way that they all share state and the first declared one is the primary while future declared ones are replicas. This would let you declare them wherever you need them and bind to the values via Polymer's normal ways.
But to answer your question, there's currently no way to easily programmatically setup the same kind of binding without using private Polymer APIs.
To avoid repeating as much and for the binding issue you were having you could use Polymer's built-in listen and unlisten methods:
Polymer({
is: 'my-component',
properties: {
api: {
observer: '_onApiChanged',
type: HTMLElement
},
products: {
type: Array
},
users: {
type: Array
}
},
_onApiChanged: function(newVal, oldVal) {
var apiProperties = ['users', 'products'];
if (oldVal) {
apiProperties.forEach(function(prop) {
this.unlisten(oldVal, prop + '-changed', '_onDataChanged');
});
}
// Listen for data changes
apiProperties.forEach(function(prop) {
this.listen(newVal, prop + '-changed', '_onDataChanged');
});
// Forward API object to children
this.$.child1.api = newVal;
this.$.child2.api = newVal;
...
},
_onDataChanged: function() {
this.users = this.api.users; // `this` should be the element now
this.products = this.api.products;
}
});
Given how this is a common pattern you're doing, you could probably get a lot of benefit out of extracting some of these things into a Behavior that abstracts away the binding/unbinding and API element forwarding.
Another optimization you may could make work would be to to look at the event passed to _onDataChanged to see if you can infer which value changed and update your corresponding property. This could prevent you needing to add a line for every property.
I ended up using an other solution. Instead of manually passing the top <my-api> element down the hierarchy any element that needs access to this shared data declares its own <my-api>.
Then in the <my-api> element's declaration I made that all instances use the same arrays references. So whenever I update one they all get updated, and I don't have to pass anything down the HTML hierarchy, which makes things a LOT simpler.

Watching for model change

I have the following code in my directive's link function:
link: function (scope, elem, attrs, ngModel) {
$(elem).datagrid({
columns: [[
{ field: 'ck', checkbox: 'true' },
{ field: 'ProjectID', title: 'Project ID', width: '30%' },
{ field: 'Name', title: 'Name' }
]]
});
ngModel.$render = function (value) {
$(elem).datagrid('loadData', ngModel.$viewValue);
};
scope.$watch('projectList', function (newValue, oldValue) {
$(elem).datagrid('loadData', ngModel.$viewValue);
});
}
When Array $scope.projectList is initially assigned with data both listeners are fired. Somewhere in my controller (just for testing) I am adding another element to $scope.projectList:
$scope.test = function () {
var project = $scope.projectList[0];
$scope.projectList.push(project);
}
At this point none of listeners are fired.Can someone please explain why that is happening?
Thanks
$watch is only checking if the reference to the projectList array has changed, it does not perform a deep watch on the collection. When you assign the array to the scope variable, you change this reference, but subsequently modifying this array leaves the reference intact. In your case, using the $watchCollection() method seems more suitable.
it's worth noting, though, that $watchCollection only checks if the collection element references have changed, e.g. by adding/removing/replacing an item. It does not check if those elements themselves have been modified.
If you want to have a deep watch on your collection, pass a true as the third parameter to $watch().
scope.$watch('projectList', function (newValue, oldValue) {
$(elem).datagrid('loadData', ngModel.$viewValue);
}, true); // <--- note the objectEquality flag set to true
Note, however, that this might have performance implications if the items in the collection are complex and require more time to compare them.
You can also check Angular docs for $scope for more information (scroll down a bit for $watch() and $watchCollection() method descriptions).
This is because the normal $watch function just looks at reference equality, so if you did something like this:
var project = $scope.projectList[0];
$scope.newProjectList = [];
$scope.newProjectList.push(project);
$scope.projectList = $scope.newProjectList;
Then it would trigger your watch because the object reference of $scope.projectList changed.
If you wanted your example:
var project = $scope.projectList[0];
$scope.projectList.push(project);
to trigger the watch, then you would either have to do
scope.$watch('projectList', function (newValue, oldValue) {
$(elem).datagrid('loadData', ngModel.$viewValue);
}, true);
(Passing true as the last argument to $watch causes $watch to do a deep equality comparison, which can be slow with big objects or large lists)
OR
scope.$watchCollection('projectList', function (newValue, oldValue) {
$(elem).datagrid('loadData', ngModel.$viewValue);
});
(This is similar to the regular $watch in terms of reference equality, but it was made especially for lists. So, on top of the main reference check, it also does a reference check of each of the items in the collection or array, so it would trigger from things like .push and .pop)
They all have their advantages, depending on what kind of checks you're looking for. Also, remember that the $watch returns a deregister function that you can use to clear it out, which you would usually do inside scope.$on('$destroy'. If you don't, they just stay around for a while and can be a drain if you have a lot.
Here's a good
article on all the differences between the 3 flavors of watch

Categories

Resources