I am trying to use a Vue watcher on a computed object and it is not working at all. It works properly if it's just a string, but not if it's an object. I followed the Vue documentation but I am still getting nothing logged to the console when the object changes. The properties of the object and computed property are changing, as I have confirmed in Vue Tools. What am I doing wrong? Thanks
<v-btn small dark #click="test1.asdf = 'blah'">Test</v-btn>
data() {
return {
test1: {},
}
},
computed: {
test() {
return this.test1
}
},
watch: {
test: {
handler: function(val, oldVal) {
console.log(oldVal, val);
},
deep: true
}
}
Try this code, its works fine
<template>
<div id="app">
{{ test1 }}
<hr />
<button #click="test1.asdf = 'blah'">Click</button>
</div>
</template>
<script >
export default {
data() {
return {
test1: { asdf: "HEY" },
};
},
computed: {
test() {
return this.test1;
},
},
watch: {
test: {
handler: function (val, oldVal) {
console.log(oldVal, val);
},
deep: true,
},
},
};
</script>
I'd guess in your case you should add .native at end of your click event like this #click.native="test1.asdf = 'blah'"
Just tried by replacing the whole object. It works pretty well, and there is no need to initialize your object with the asdf property in your data:
<v-btn small dark #click="test1 = { ...test1, asdf: 'blah'}">Test</v-btn>
The spread syntax ...test1 helps keeping other properties in the target object if there is any, so you can safely add the asdf property without worrying about losing anything.
JSFiddle: https://jsfiddle.net/vh72a3bs/22/
#BadCoder Objects and Arrays are pass by reference in JavaScript, not pass by value. This means that when you add a key to the object as you are you doing in your question, you're just adding to the same Object. It's contents have changed but your variable test1 is still referencing the original object and unaware that its contents have updated. The watcher doesn't pick this change up. You can add deep: true to your watcher as another answerer has suggested, but this only watches for a couple of levels deep, so not ideal if you have a large object with lots of nested data. The most reliable way to trigger a watcher when dealing with arrays or objects is to create a new instance of that object. You can achieve this with object destructing.
Something like,
<v-btn small dark #click="test1 = { ...test1, asdf: 'blah'}">Test</v-btn>
works because you're creating a new object (using the previous objects attributes, plus anything new), triggering the watcher.
Related
<el-form-item label="range"
v-if="isShowDatepicker">
<el-date-picker type="datetimerange"
style="width: 100%"
v-model="timeRange"
range-separator="to"
start-placeholder="start"
end-placeholder="end"></el-date-picker>
</el-form-item>
I have a form that has an item to input date and time (just like the code above), and I want to obtain the value after it has changed. I want to use Vue watch to accomplish this task. However, I don't know how to name the variable in watch. I've tried this:
"SearchForm.timeRange": function (val) {
console.log(val)
}
To watch data changes using the watch object, add a function to that object with a name matching the data property to watch. For instance, to watch a data property named "question", add this function:
watch: {
question(newValue, oldValue) {
//...
}
}
Alternatively, you could add an object named after the target data property, with a handler function and additional watcher options: deep and immediate.
watch: {
question: {
deep: true, // detecting nested changes in objects
immediate: true, // triggering the handler immediately with the current value
handler(newValue, oldValue) {
//...
}
}
}
In your case with "timeRange", the syntax would be:
watch: {
timeRange(newValue) {
//...
}
}
demo
Im trying to use a parent's mixin as a child model, but I couldn't make it work as the mixin isn't resolved on the 'data' definition.
Here is a fiddle:
<div id="vue-instance">
<div>
USER: {{user}}
<br/>
EMAIL: {{email}}
</div>
<input-list :field="field" v-for="field in fields"/>
</div>
js:
Vue.component('input-list', {
name: 'InputList',
props: ['field'],
template: '<div>{{field.id}}: <input type="text" v-model="field.model"/></div>'
})
var userData = {
data() {
return {
user: 'foo',
email: 'foo#barbar'
}
}
}
var vm = new Vue({
el: '#vue-instance',
mixins: [userData],
data() {
return {
fields: [
{
id: "UserName",
model: this.user
},
{
id: "Email",
model: this.email
}
]
}
}
});
https://jsfiddle.net/rafalages/rp7bu2qt/9/
The expected result would be update parent mixin value in the child input.
This is not an issue with the mixin, but rather the overall design, if you we to use data instead of mixin, you'd see the same behaviour.
This will not work they way you intend it to.
Two things about Vue worth re-iterating:
You pass props down and emit events up
Mutating a prop will not update the parent
more reading here:
https://v2.vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow
There is a note at the bottom
Note that objects and arrays in JavaScript are passed by reference, so if the prop is an array or object, mutating the object or array itself inside the child component will affect parent state.
But my recommendation is to think of this as a side-effect rather than a feature. You should not rely on the child from updating the value. The only reliable way (outside of other state management) is to pass a function.
Another issue is that referencing this in data during creation will not work
data: {
fields: [
{
id: "UserName",
model: this.user
},
{
id: "Email",
model: this.email
}
]
}
This will not use the data from this.user and will be set to null. You can use the mounted life-cycle function to set these like this:
mounted() {
this.fields = [
{
id: "UserName",
model: this.user
},
{
id: "Email",
model: this.email
},
]
}
which will set it to use the initial values. But referencing an object like this inside another object will also serve to create new bindings, essentially cutting the reactivity. This means that you'll use the values from those objects, but changes to this.fields[1].model will not update this.email
here is a semi-working fiddle that attempts to use your current design
https://jsfiddle.net/rp7bu2qt/113/
notice that...
the binding is using v-model, which is shorthand for binding a prop and an emit/update to a value
you see an error
You are binding v-model directly to a v-for iteration alias. This will not be able to modify the v-for source array because writing to the alias is like modifying a function local variable. Consider using an array of objects and use v-model on an object property instead.
changes to the input fields do not change the original data, just the content of fields[n].model
I'm working on a project, similar as a bill manager, so I want that the subtotal get recalculated every time that quantity or unit value change, I have tried and searched to accomplish this using watcher or computed properties, but I don't find the right approach, cause I need to access the whole scope of the element when another change, like this.
Model structure:
detail
quantity
unit value
subtotal (should be a computed or updated)
So I think I should be able of doing something like this:
Vue.component('item', {
template: '#item',
props: {
item: Object,
},
computed:{
total: function(){
return this.quantity*this.unit_value;
}
},
watch:{
'item.quantity':()=>{
this.subtotal = this.quantity*this.unit_value;
}
}
});
I have several components being read from a list
I merged the approach using watcher and computed in the same code to make it shorter.
The problem is that I haven't found a way to access the hole element from inside itself, anyone could pls explain the right way? thanks
You shouldn't use arrows functions there, use method declarations.
If you want to watch for a property of the item object, you'll have to watch for the item object itself, and additionally use the deep: true flag of the watcher.
Final detail, you are using several properties that are not declared in your data. Declare them, otherwise they will not be reactive, that is, the computed will not recalculate when they change.
See code:
Vue.component('item', {
template: '#item',
props: {
item: Object,
},
data() {
return {
subtotal: null, // added data properties
quantity: null,
unit_value: null
}
},
computed: {
total: function() {
return this.quantity * this.unit_value;
}
},
watch: {
item: { // watching for item now
deep: true, // using deep: true
handler() { // and NOT using arrow functions
this.subtotal = this.quantity * this.unit_value;
}
}
}
});
As the title says, I'm trying to change the value of a prop/data in a component, but the trigger is being fired from outside the component, from something that has nothing to do with Vuejs.
Currently I trying to use a Simple State Manager, based on the docs from here, like so:
var store = {
debug: true,
state: {
progress: 23
},
setProgress (uff) {
if (this.debug) console.log(uff)
this.state.progress = uff
}
}
The documentation leads me to believe that if the value of progress is mutated, the value of my Vue instance would also change if I link them accordingly. But this doesn't work in a component (my guess would be it's cause it's a function).
This is part of my component:
Vue.component('transcoding', {
data () {
return {
progress: store.state.progress
}
},
template: `
<v-progress-circular
:size="130"
:width="25"
:value="progress"
color="teal"
>
{{progress}}
</v-progress-circular>
`
})
So, when I trigger a store.setProgress(value), nothing happens. The log messages do happen, but the state isn't updated in the component.
This is the script that's currently triggering the state change:
App.cable.subscriptions.create(
{ channel: "ProgressChannel", room: "2" },
{ received: function() {
store.setProgress(arguments[0])
}}
)
It's an ActionCable websocket in Ruby on Rails. The trigger works perfectly, but I just cannot make the connection between the state change and the component.
I tried loading this script in the mounted() event for the component, thinking I could reference the value like this:
Vue.component('transcoding', {
data () {
return {
progress: 0
}
},
template: `
<v-progress-circular
:size="130"
:width="25"
:value="progress"
color="teal"
>
{{progress}}
</v-progress-circular>
`,
methods: {
setProgress: function(uff) {
this.progress = uff
}
},
mounted() {
App.cable.subscriptions.create(
{ channel: "ProgressChannel", room: "2" },
{ received: function() {
this.setProgress(arguments[0])
}}
)
}
})
But this gives me an error saying that this.setProgress is not a function, which is obvious since I'm calling it within the create method of App.cable.subscriptions.
How can I make this work? I realize I'm mixing things with my question, but I wanted to illustrate what my goal is. I simply want to know how to make the component's progress data to update, either from the outside, or from the component itself if I can make it find the function.
You are initializing your data item to the value from the store:
data () {
return {
progress: store.state.progress
}
}
Changes to the store will not propagate to your data item. You could eliminate the data item and just use store.state.progress where you need it, or you could create an computed that returns its value if you want a local single-name handle for it.
How do I access $refs inside computed? It's always undefined the first time the computed property is run.
Going to answer my own question here, I couldn't find a satisfactory answer anywhere else. Sometimes you just need access to a dom element to make some calculations. Hopefully this is helpful to others.
I had to trick Vue to update the computed property once the component was mounted.
Vue.component('my-component', {
data(){
return {
isMounted: false
}
},
computed:{
property(){
if(!this.isMounted)
return;
// this.$refs is available
}
},
mounted(){
this.isMounted = true;
}
})
I think it is important to quote the Vue js guide:
$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.
It is therefore not something you're supposed to do, although you can always hack your way around it.
If you need the $refs after an v-if you could use the updated() hook.
<div v-if="myProp"></div>
updated() {
if (!this.myProp) return;
/// this.$refs is available
},
I just came with this same problem and realized that this is the type of situation that computed properties will not work.
According to the current documentation (https://v2.vuejs.org/v2/guide/computed.html):
"[...]Instead of a computed property, we can define the same function as a method. For the end result, the two approaches are indeed exactly the same. However, the difference is that computed properties are cached based on their reactive dependencies. A computed property will only re-evaluate when some of its reactive dependencies have changed"
So, what (probably) happen in these situations is that finishing the mounted lifecycle of the component and setting the refs doesn't count as a reactive change on the dependencies of the computed property.
For example, in my case I have a button that need to be disabled when there is no selected row in my ref table.
So, this code will not work:
<button :disabled="!anySelected">Test</button>
computed: {
anySelected () {
if (!this.$refs.table) return false
return this.$refs.table.selected.length > 0
}
}
What you can do is replace the computed property to a method, and that should work properly:
<button :disabled="!anySelected()">Test</button>
methods: {
anySelected () {
if (!this.$refs.table) return false
return this.$refs.table.selected.length > 0
}
}
For others users like me that need just pass some data to prop, I used data instead of computed
Vue.component('my-component', {
data(){
return {
myProp: null
}
},
mounted(){
this.myProp= 'hello'
//$refs is available
// this.myProp is reactive, bind will work to property
}
})
Use property binding if you want. :disabled prop is reactive in this case
<button :disabled="$refs.email ? $refs.email.$v.$invalid : true">Login</button>
But to check two fields i found no other way as dummy method:
<button :disabled="$refs.password ? checkIsValid($refs.email.$v.$invalid, $refs.password.$v.$invalid) : true">
{{data.submitButton.value}}
</button>
methods: {
checkIsValid(email, password) {
return email || password;
}
}
I was in a similar situation and I fixed it with:
data: () => {
return {
foo: null,
}, // data
And then you watch the variable:
watch: {
foo: function() {
if(this.$refs)
this.myVideo = this.$refs.webcam.$el;
return null;
},
} // watch
Notice the if that evaluates the existence of this.$refs and when it changes you get your data.
What I did is to store the references into a data property. Then, I populate this data attribute in mounted event.
data() {
return {
childComps: [] // reference to child comps
}
},
methods: {
// method to populate the data array
getChildComponent() {
var listComps = [];
if (this.$refs && this.$refs.childComps) {
this.$refs.childComps.forEach(comp => {
listComps.push(comp);
});
}
return this.childComps = listComps;
}
},
mounted() {
// Populates only when it is mounted
this.getChildComponent();
},
computed: {
propBasedOnComps() {
var total = 0;
// reference not to $refs but to data childComps array
this.childComps.forEach(comp => {
total += comp.compPropOrMethod;
});
return total;
}
}
Another approach is to avoid $refs completely and just subscribe to events from the child component.
It requires an explicit setter in the child component, but it is reactive and not dependent on mount timing.
Parent component:
<script>
{
data() {
return {
childFoo: null,
}
}
}
</script>
<template>
<div>
<Child #foo="childFoo = $event" />
<!-- reacts to the child foo property -->
{{ childFoo }}
</div>
</template>
Child component:
{
data() {
const data = {
foo: null,
}
this.$emit('foo', data)
return data
},
emits: ['foo'],
methods: {
setFoo(foo) {
this.foo = foo
this.$emit('foo', foo)
}
}
}
<!-- template that calls setFoo e.g. on click -->