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)" />
Related
I'm defining my state with an object, initialized with some nested objects to an empty string and an empty array, as such:
state: {
displayedFarmer: {
name: "",
arrivalDates: []
// some more fields...
},
// more vuex stuff
}
I would expect that if I console.log the displayed farmer, arrivalDates would appear. Here's what I did to track it in my component:
computed: {
...mapState([ 'displayedFarmer' ]),
// more code
},
watch: {
displayedFarmer: {
handler() {
console.log("displayedFarmer", this.displayedFarmer);
},
deep: true,
immediate: true
}
}
The first log line appearing shows the displayedFarmer object, with the arrivalDates and name missing:
displayedFarmer
Object { … }
(basically only the prototype and the __ob__ objects appear when I expand it in the console)
That behavior is unclear to me, and has forced me to use a small and harmless hack to initialize the fields the first time they are being accessed.
For this question, what I want to know is:
Why can't I see the objects I initialized in my state when I access them via the component?
How can I do this differently, so that when I first access the object, all the nested items are initialized?
watch with immediate: true fires before components hook:created. This is probably the reason you don't have access to the initialized object.
I don't know what you are trying to achieve, can you expand your question?
The first thing that came to my mind is to try to call method (which does whatever you want) on a hook:mouted. Then call the same method in watcher, but with immediate: false (which is a default btw)
I am looking to use custom Javascript to interact with form fields tied with Vue framework. The form appears in a WordPress theme search page (https://wilcity.com/search-without-map/)
Autoselect the region value (this I can perform using the JS below)
markerCityName = "Atlanta";
for (i = 0; i < document.getElementsByClassName("wilcity-select-2 select2-hidden-accessible")[0].options.length; i++) {
if (document.getElementsByClassName("wilcity-select-2 select2-hidden-accessible")[0].options[i].innerText == markerCityName) {
document.getElementsByClassName("wilcity-select-2 select2-hidden-accessible")[0].selectedIndex =
document.getElementsByClassName("wilcity-select-2 select2-hidden-accessible")[0].options[i].index;
triggerEvent(document.getElementsByClassName("select2-selection select2-selection--single")[0], 'focus');
triggerEvent(document.getElementsByClassName("select2-selection select2-selection--single")[0], 'keydown');
triggerEvent(document.getElementsByClassName("wilcity-select-2 select2-hidden-accessible")[0], 'change');
}
}
Selected value participates in the search without having to manually select it from the form interface.
(i) After the javascript runs and selects "Atlanta" in the region drop down.
(ii) Select any other field in the form for search to be executed.
(iii) you will notice this search did not take into account the pre-select region value "Atlanta"
I am unable to do (2). The autoselected value is not sent in post when form value changes, and the autoselected value is not picked up.
Modifying the DOM directly won't work, as you've discovered, because Vue doesn't know about those changes and is still working based on its internal state. You need to modify Vue's underlying data model instead.
Every Vue component's root DOM element will have a __vue__ property attached, which you can use to access and modify the component's internal state from outside:
// Set up a Vue component with some data in it:
Vue.component('child', {
data() {
return {
foo: 'Data from inside Vue'
}
},
template: '<div id="component">{{foo}}</div>'
})
new Vue({
el: '#app',
});
// now outside Vue:
document.getElementById('component').__vue__.$data.foo = "Updated value from outside vue"
<script src="https://unpkg.com/vue#latest/dist/vue.min.js"></script>
<div id="app">
<child></child>
</div>
Use of the __vue__ property isn't officially supported, as far as I know, but Vue's author says it's safe to use:
the official devtool relies on it too, so it's unlikely to change or break.
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.
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.
Disclaimer: This is my first attempt at building an MVVM app I have also not worked with vue.js before, so it could well be that my issue is a result of a more fundamental problem.
In my view I have two types of blocks with checkboxes:
Type 1: block/checkboxes
Type 2: block/headers/checkboxes
The underlying object is structured like this:
{
"someTopLevelSetting": "someValue",
"blocks": [
{
"name": "someBlockName",
"categryLevel": "false",
"variables": [
{
"name": "someVarName",
"value": "someVarValue",
"selected": false,
"disabled": false
}
]
},
{
"name": "someOtherBlockName",
"categryLevel": "true",
"variables": [
{
"name": "someVarName",
"value": "someVarValue",
"categories": [
{
"name": "SomeCatName",
"value": "someCatValue",
"selected": false,
"disabled": false
}
]
}
]
}
]
}
My objectives
Selecting checkboxes:
User clicks on checkbox, checkbox is selected (selected=true)
A method is fired to check if any other checkboxes need to be disabled (disabled=true). (If this method has indeed disabled anything, it also calls itself again, because other items could be in turn dependent on the disabled item)
Another method updates some other things, like icons etc
Clearing checkboxes
A user can click on a "clear" button, which unchecks all checkboxes in a list (selected=false). This action should also trigger the methods that optionally disables checkboxes and updates icons etc.
My current method (which doesn't seem quite right)
The selected attribute of the data-model is bound to the checked
state of the checkbox element via the v-model directive.
The disabled attribute (from the model) is bound to the element's class and disabled attribute. This state is set by the aforementioned method.
To initialize the methods that disable checkboxes and change some icons, I am using a v-on="change: checkboxChange(this)" directive.
I think I need to do this part differently
The clearList method is called via v-on="click: clearList(this)"
The problems with my current setup is that the change event is not firing when the checkboxes are cleared programatically (i.e. not by user interaction).
What I would like instead
To me the most logical thing to do would be to use this.$watch and keep track of changes in the model, instead of listening for DOM events.
Once there is a change I would then need to identify which exact item changed, and act on that. I have tried to create a $watch function that observes the blocks array. This seems to pick up on the changes fine, but it is returning the full object, as opposed to the individual attribute that has changed. Also this object lacks some convenient helper attributes, like $parent.
I can think of some hacky ways to make the app work (like manually firing change events in my clearList method, etc.) but my use case seems pretty standard, so I expect there is probably a much more elegant way to handle this.
You could use the 'watch' method.. for example if your data is:
data: {
block: {
checkbox: {
active:false
},
someotherprop: {
changeme: 0
}
}
}
You could do something like this:
data: {...},
watch: {
'block.checkbox.active': function() {
// checkbox active state has changed
this.block.someotherprop.changeme = 5;
}
}
If you want to watch the object as a whole with all its properties, and not only just one property, you can do this instead:
data() {
return {
object: {
prop1: "a",
prop2: "b",
}
}
},
watch: {
object: {
handler(newVal, oldVal) {
// do something with the object
},
deep: true,
},
},
notice handler and deep: true
If you only want to watch prop1 you can do:
watch: {
'object.prop1' : function(newVal, oldVal) {
// do something here
}
}
Other solution not mentioned here:
Use the deep option.
watch:{
block: {
handler: function () {console.log("changed") },
deep: true
}
}
Since nobody replied and I have solved/ worked around the issue by now, I thought it migth be useful to post my solution. Please note that I am not sure my solution is how these types of things should be tackled, it works though.
Instead of using this event listener v-on="change: checkboxChange(this)" I am now using a custom directive which listens to both the selected and disabled model attribute, like this: v-on-filter-change="selected, disabled".
The directive looks like this:
directives: {
'on-filter-change': function(newVal, oldVal) {
// When the input elements are first rendered, the on-filter-change directive is called as well,
// but I only want stuff to happen when a user does someting, so I return when there is no valid old value
if (typeof oldVal === 'undefined') {
return false;
}
// Do stuff here
// this.vm is a handy attribute that contains some vue instance information as well as the current object
// this.expression is another useful attribute with which you can assess which event has taken place
}
},
The if clause seems a bit hacky, but I couldn't find another way. At least it all works.
Perhaps this will be useful to someone in the future.