Access component instance from vue watcher - vue.js

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

Related

vue watch handler called without actual changes

In this codepen have a counter that can be incremented with a "incr" link.
I now have a computed property and a watch:
computed: {
test() {
let unused = this.counter;
return [42];
}
},
watch: {
test(val, old) {
// Should I avoid firing when nothing actually changed
// by implementiong my own poor-man's change detection?
//
// if (JSON.stringify(newVal) == JSON.stringify(oldVal))
// return;
console.log(
'test changed',
val,
old
);
}
}
A contrived example perhaps, but in reality this is a calculation where the real data is reduced (in a vuex getter) and most-often, the reduced data doesn't change even when some of the data changes.
Edited to add more detail: The data in the vuex store is normalized. We're also using vue-grid-layout that expects its layoutproperty in a certain non-normalized format. So we have a gridLayout getter that does the vuex -> vue-grid-layout tranform. Watching this gridLayout getter fires even when the resulting gridLayout doesn't actually change, but other details do, such as names and other irrelevant-to-vue-grid-layout object keys in the vuex store.
Now in the above example, when this.counter changes, the watch on test fires too, even though the newVal and oldVal are "the same". They aren't == or === mind you, but "the same" as in JSON.stringify(newVal) == JSON.stringify(oldVal).
Is there any way to have my watch fire only when there are actual changes? Actually comparing JSON.stringify() seems inefficient to me, but I'm worried about performance problems as my project grows as my watch could do expensive operations and I want to ensure I'm not missing something.
According to the Vue.js documentation computed properties are reevaluated when ever a reactive dependency is changed.
In your case the reactive dependency is this.counter
You can achieve the same result by invoking a method as opposed to a computed property.
Just change up your component architecture:
data () {
return: {
counter: 0,
obj: {},
output: null
}
},
watch: {
counter(val, old) {
// Alternatively you could remove your method and do something here if it is small
this.test(val);
},
// Deep watcher
obj: {
handler: function (val, oldVal) {
console.log(val);
console.log(oldVal);
this.test(val);
},
deep: true
},
}
},
methods: {
test() {
let unused = this.counter;
if (something changed)
this.output = [42];
}
}
}
Now in your template (or other computed properties) output is reactive
Read more: https://v2.vuejs.org/v2/guide/computed.html#Computed-Caching-vs-Methods

Filter in PROPS in vue.js

I have code like below
created () {
EventBus.$on('change',this.formated);
},
props: ['applicants',],
data() {
return {
values: [],
}
},
methods : {
formated (item) {
//some code here
}
}
I have some checkboxes in another component. I am trying to catch values of those checkboxes in this component. After catching those values I would like to filter the props applicants. I would like to find out which applicants have those values of checkboxes. Then I would like to pass them to values[]. Then I would like to iterate those values in HTML.
After reading the comments I believe this is what you need to do. You can use the prop as well as the data passed through the event bus in the formatted() method. You need to combine the two and set them to values. I have pasted code below. However, not knowing the structure of either two objects I can't help you further. This is just plain JavaScript. So probably you need something like Object.assign or the .filter or .map methods.
created () {
EventBus.$on('change',this.formatted);
},
props: ['applicants'],
data() {
return {
values: [],
}
},
methods : {
formatted (item) {
this.applicants // access the data from your prop
item // access the data from your event bus
this.values = someCombinationOf(this.applicants, item)
}
}
After setting values from the formatted () function you can iterate over it (for example with v-for). Whenever an event is emitted through the event bus this will update your component. Whenever the prop changes the component will be updated also.

How to change the value of a prop (or data) of a component, from OUTSIDE the component?

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.

Using $refs in a computed property

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 -->

Vuejs deep nested computed properties

I'm not really understanding where to put function() { return {} } and where not to when it comes to deeply nesting computed properties.
By the way, this is in a component!
computed: {
styles: function() {
return {
slider: function() {
return {
height: {
cache: false,
get: function() {
return 'auto';
}
},
width: {
cache: false,
get: function() {
return $('#slideshow').width();
}
}
}
}
}
}
},
This is returning undefined. When I get rid of the function() { return {} } inside of the slider index, it returns an object when I do styles.slider.width instead of the get() return. It just shows an object with cache and get as indexes..
Thanks for any help!
The reason I'm asking is because I have multiple nested components that involve styling from the parent. Slider, tabs, carousels, etc. So I wanted to organize them like this.
I believe you mean to return a computed object, but not actually structure the computation in a nested manner?
What the others have said regarding the 'computed' hook not having syntax for nesting is correct, you will likely need to structure it differently.
This may work for you: I generate many objects in a similar fashion.
computed: {
computedStyles(){
var style = {slider:{}}
style.slider.height = 'auto'
style.slider.width = this.computedSlideshowWidth
return style
},
computedSlideshowWidth(){
return $('#slideshow').width()
}
As per 2020 and Vue 2.6.12 this is completelly possible. I believe this has been possible since v.2 but cannot confirm.
Here is the working example:
this.computed = {
// One level deep nested,
// get these at `iscomplete.experience`
// or `iscomplete.volume`
iscomplete: function() {
return {
experience: this.$data.experience !== null,
volume: this.$data.volume > 100,
// etc like this
};
},
// More levels deep nested.
// Get these at `istemp.value.v1 and `istemp.value.v2`
istemp: function() {
return {
value1: {
v1: this.$data.experience,
v2: 'constant'
}
}
}
};
As a result you will be able to access your deep nested computed in your template as e.g. follows <span v-text="iscomplete.experience"></span> that will output <span>true</span> for the first example computed above.
Note that:
Since Vue v.2 cache key is deprecated;
Vue would not execute functions assigned to a computed object nested keys;
You cannot have computed for non-Vue-reactive things which in your case is e.g. $('#slideshow').width(). This means they are not going to be re-computed on their content change in this case (which is a computed's sole purpose). Hence these should be taken away from computed key.
Other than that I find nested computeds to be quite helpful sometimes to keep things in better order.