Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

$watch multiple properties for single callback #844

Closed
nadirabid opened this issue May 16, 2015 · 27 comments
Closed

$watch multiple properties for single callback #844

nadirabid opened this issue May 16, 2015 · 27 comments

Comments

@nadirabid
Copy link

What do you think about being able to $watch over an array of data properties for a given callback?

$watch(['x', 'y', 'z'], function() { 
  // do something 
});

// or 

$watch('x', 'y', 'z', function() { 
  // do something
});

Not exactly pressing as I can easily make do with multiple $watch statements.

@yyx990803
Copy link
Member

I don't think this is an improvement significant enough for further complicating the API, but thanks for the suggestion :)

@devinfd
Copy link

devinfd commented Jul 9, 2015

I know this is an old request but I've just encountered this "need" and it would be super nice to have. Rather than doing:

$watch('x', function() { 
  this.doSomething(); 
});
$watch('y', function() { 
  this.doSomething(); 
});
$watch('z', function() { 
  this.doSomething(); 
});

@nirazul
Copy link

nirazul commented Jul 9, 2015

You could easily do something like this:

...
methods: {
    baywatch: function(arr, cb) {
        var self = this;
        arr.forEach(function(val) {
            self.$watch(val, cb.bind(self));
        })
    }
},
...

...and then do: this.baywatch(['a', 'b', 'c'], this.doSomething);.
You could even make a mixin out of it, so you can use it everywhere you want.

@young-steveo
Copy link

@nirazul 's solution is good, but you should maybe leave the context binding to the calling code to be more useful as a mixin:

methods : {
    baywatch : function(props, watcher) {
        var iterator = function(prop) {
            this.$watch(prop, watcher);
        };
        props.forEach(iterator, this);
    }
},

//...

this.baywatch(['a', 'b', 'c'], this.doSomething.bind(this));

@devinfd
Copy link

devinfd commented Jul 9, 2015

Yes this is a viable solution.

@Gubbi
Copy link

Gubbi commented May 31, 2016

How can the above be done during extend and not on per instance basis?

@thattomperson
Copy link

thattomperson commented May 31, 2016

You could look into making a plugin
http://vuejs.org/guide/plugins.html

I can't write an example at the moment but look at some of the other plugins on that page and see how they add methods to the Vue.prototype

edit:
or just super hacky

var Vue = require('vue')
Vue.prototype.$baywatch = function () {
    var iterator = function(prop) {
        this.$watch(prop, watcher);
    };
    props.forEach(iterator, this);
}

@PeppeL-G
Copy link

PeppeL-G commented Dec 4, 2016

I was looking for this feature as well.

I have three data variables binded to three input elements in a form, and I need to listen for changes to any of these three data variables and then update a canvas through a third party library. Using watch and pass it an array of data variables that would trigger my single watch function does seem like the simplest solution to me. Does anyone has another solution? I don't want to rely on a plugin/own code modifying Vue for this simple case.

@itsMapleLeaf
Copy link
Contributor

$watch('x', 'y', 'z', function() { 
  // do something
})

This form seems sensible enough to implement. If it's not hard to document, I'm for it.

@sirlancelot
Copy link

sirlancelot commented Dec 7, 2016

For this, I would just build a computed property which is derived from your desired properties, then watch that:

const $app = new Vue({
    computed: {
        compoundProperty() {
            // `.join()` because we don't care about the return value.
            return [this.x, this.y, this.z].join()
        }
    },
    watch: {
        compundProperty() {
            // do something
        }
    }
})

UPDATE To skip the performance penalty of allocating and discarding an array, you can comma-separate all the dependent values, and finally return something that will always be different (Date.now() taking from comments below this):

computed: {
    computedProperty() {
        return this.x, this.y, this.z, Date.now();
    }
}

Some linters might complain, but it's perfectly valid JS.

@amirrustam
Copy link

@sirlancelot Very nice solution.

Whenever I need a computed property to accomplish this, I append Watchable to the computed property name. So a computed myProperty would become myPropertyWatchable. This is personal convention, and I'm sharing in case someone out there finds it useful.

@fnlctrl
Copy link
Member

fnlctrl commented Jan 8, 2017

It can be made simpler to just reference the reactive properties, and then return a value that's guaranteed to be different every time.
This way we avoid a performance overhead of an array operation in the original example by @sirlancelot, though it should be trivial in most cases.
Also, if x, y, z are objects then [this.x, this.y, this.z].join() would give the same result "[object Object],[object Object],[object Object]" every time x/y/z is changed, which prevents the compoundProperty watcher from being fired.

new Vue({
    computed: {
        compoundProperty() {
            // it's only required to reference those properties
            this.x;
            this.y;
            this.z;
            // and then return a different value every time
            return Date.now() // or performance.now()

            // object literals also work but we don't need that overhead
            // return {}
            // return []
        }
    },
    watch: {
        compoundProperty() {
            // do something
        }
    }
})

@simplesmiler
Copy link
Member

simplesmiler commented Jan 8, 2017

Here's my two cents.

In 1.x watchers supported expressions, so you could globally mix in a $touch method that would just return Date.now() (or in other way generate unique value), and then define a watcher for $touch(x, y, z), and it would be called every time x, y or z were changed. Demo: https://jsfiddle.net/pdjygyhg/3/

In 2.x you can not just watch an expression, and have to define a computed prop to watch instead. So the best way I can think of is to ease the creation of computed field: https://jsfiddle.net/kmj6Lsae/3/

@ndabAP
Copy link

ndabAP commented Mar 19, 2017

Just wanted to also add my two cents.

I got a situation where I also have to watch for two variables. But as already mentioned: I don't think that if it would be possible natively this would be a significant improvement, too.

@g8up
Copy link

g8up commented Jun 11, 2017

and how to debounce the callback function ?

@DrSensor
Copy link

DrSensor commented Sep 10, 2017

I also encounter a similar situation when I need to watch multiple state change in some component.

  computed: {
    ...mapGetters('bim', [
      'folders',
      'items'
    ]),
    ...mapState('bim', [
      'projects',
      'activeProject'
    ])
  },
  watch: {
    // wait listProjects action to be completed before listing folder
    projects: function (projectObj) {
      this.listFolders()
      this.listItems()
    },
    activeProject: function (projectName) {
      this.listFolders()
      this.listItems()
    }
  },

@SergioCrisostomo
Copy link

SergioCrisostomo commented Sep 13, 2017

@sirlancelot very interesting solution! +1

A small suggestion would be to use a Object (new Date();) as return value, since Date.now(); is Number and can give same result if things happens fast.

Buggy example: https://jsfiddle.net/Sergio_fiddle/hdqgyLvx/
Consistent example: https://jsfiddle.net/Sergio_fiddle/hdqgyLvx/1/

@bigsee
Copy link

bigsee commented Oct 8, 2017

@sirlancelot thanks for a nice solution! +1

@yyx990803
Copy link
Member

yyx990803 commented Oct 8, 2017

FYI you can directly watch a getter via $watch without the need for a separate computed property:

this.$watch(vm => [vm.x, vm.y, vm.z].join(), val => {
  // ...
})

https://vuejs.org/v2/api/#vm-watch

@btoo
Copy link

btoo commented Feb 16, 2018

i think @sirlancelot and @yyx990803 's solutions pose risks to performance because you have to construct an entirely new object just for the sake of hashing multiple watchers, which is particularly dangerous when you want to hash a large number of watchers together. wouldnt it be safer to define all the watchers in the beginning when the component is constructed? with the spread operator and a simple reducer, this can be done with relatively little clutter (my guess is this is pretty similar to how vuex does it):

watch: {
  ...[
    'key1',
    'key2',
    'key3'
  ].reduce((watchers, key) => ({
    ...watchers,
    [key](newVal, oldVal){
      // ...
    }
  }), {}),
}

it would be nice if a utility function to map watchers were provided too

import { mapWatchers } from 'vue'

allowing for something like this:

watch: {
  ...mapWatchers([
    'key1',
    'key2',
    'key3'
  ], function(newVal, oldVal){
    // ...
  })
}

or even this if we want to provide access to the key, allowing for programmatic generation of watchers:

watch: {
  ...mapWatchers([
    'key1',
    'key2',
    'key3'
  ], key => function(newVal, oldVal){
    // ...
  })
}

@vd3v
Copy link

vd3v commented Mar 13, 2018

This solution worked pretty well for me and I found it more readable then making a computed properties then watch on them.

Vue.prototype.$watchAll = function(props, callback) {
  props.forEach(prop => {
    this.$watch(prop, callback);
  });
};

and then in any component I can use it, for instance, like this

this.$watchAll(["state.price", "state.amount"], this.onStateChange);

based on @thattomperson suggestion

@guillermovs
Copy link

guillermovs commented Mar 15, 2018

Based on @denwerboy's solution - pass the watched property name to the callback function.
Useful to save changes to localStorage automatically

Vue.prototype.$watchAll = function(props, callback) {
  props.forEach(prop => {
    // Pass the prop as the first argument to our callback
    this.$watch(prop, callback.bind(null, prop));
  });
};

Then call $watchAll on mount and inspect the arguments

this.$watchAll(["field1", "field2"], function(field, value){
  console.log("changed", field, value);
  localStorage.setItem(field, JSON.stringify(value));
});

@matthew-dean
Copy link

Ractive solves watching multiple paths with the asterisk, so you could do things like:

this.$watch('*', function() { ... });
this.$watch('root.*', function() { ... });  // passes in individual sub-properties when they change, and not `root`

Seems strange you can't easily define a single callback watcher for multiple properties in Vue.

@Christilut
Copy link

Christilut commented May 12, 2018

I wanted to watch all my data props because I emit them as v-model together in a single object. I ended up going for this:

  mounted () {
    Object.keys(this._data).forEach(key => {
      this.$watch(key, () => {
        this.emit()
      })
    })
  }

@ctf0
Copy link

ctf0 commented May 19, 2018

maybe another solution could be

updated() {
    this.$nextTick(() => {
        if (this.first || this.second) {
            this.doSomething()
        }
    })
},

@sirlancelot
Copy link

Evan's solution is already the best. Here's my slight adaptation to avoid array construction:

this.$watch(
    (vm) => (vm.x, vm.y, vm.z, Date.now()),
    function () {
        // Executes if `x`, `y`, or `z` have changed.
    }
)

@posva
Copy link
Member

posva commented May 20, 2018

This thread seems to have bikeshed a lot while answers have been already provided. Let's stop it here so this doesn't get worse and to prevent spamming around 20 people 😛

See #844 (comment) and #844 (comment) for solutions

@vuejs vuejs locked and limited conversation to collaborators May 20, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests