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

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.

Related

Vue watch triggered when there is no (discernible) change to object

I have an object that I am watching in vue for the purpose of performing an action whenever a change is detected in it. Something keeps triggering it, but when I print the object to the console and compare the oldVal to newVal they seem identical.
Just looking at the objects logged to the console revealed no differences to my eye, so I thought that by stringifying them and comparing them in a text compare tool I would find differences, but there too the results were identical for code like this:
watch: {
CCompPrefs: function (newVal, oldVal) {
console.log('CC changed: ', JSON.stringify(newVal), ' | was: ', JSON.stringify(oldVal))
}
},
While not understanding why the watch was being triggered if nothing in the object had changed, I thought it was safe to do something like this:
watch: {
CCompPrefs: function (newVal, oldVal) {
if (newVal !== oldVal) {
console.log('CC CHANGED, OLD VAL DIFFERENT')
}
}
},
But the log ran, despite there being no discernible difference I could find!
So I found a working solution by doing this:
watch: {
CCompPrefs: function (newVal, oldVal) {
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
console.log('CC CHANGED, OLD VAL DIFFERENT')
}
}
},
But this still leaves me the nagging question of WHY this is being triggered in the first place. What could possibly be changing and why?
Supplementary info
CCompPrefs is coming via a computed element in the following way:
computed: {
CCompPrefs () {
return this.$store.state[this.$attrs.useCase].filter(x => (x.show === true && x.enabled === true))
},
}
Almost any action will seemingly trigger this watch. Like throwing up a model window.
Using Vue devtools, I can verify that there are NO mutations being applied to ANY part of the vuex store
UPDATE
Now I am wondering if this.$attrs.useCase in my computed value above might be the culprit. The modal I am opening is in a parent container, perhaps that switches the context for that value and forces an update? Looking into it now...
UPDATE2
Nope. this.$attrs.useCase does NOT change. So still confused, WHAT could be triggering this watcher?
I avoided redundant calls for unchanged data by crudely checking the object matches in my handler like this:
data: () => ({
lastDataString: '',
}),
itinerary: {
handler: function(v) {
// Avoid redundant calls
let dataString = JSON.stringify(v)
if (dataString === this.lastDataString){
return
}
this.lastDataString = dataString
// do stuff
},
deep: true,
},

Accessing Vuex state object property from component

I'm trying to change value of a state object property from a component but I can't find the proper way to do it
Here is my state:
state: {
articles: [ {
title: "Lorem ipsum",
position: 7
}, {
title: "Lorem ipsum",
position: 8
}
]
}
In the computed property of the component :
return this.$store.state.articles.filter((article) => {
return (article.title).match(this.search); /// just a search field, don't mind it
});
I would like to change every article.position under 10 to article.position = +'0'article.position.
I tried :
let articlesList = this.$store.state.articles
if(articlesList.position < 10) {
articlesList.position = +'0'articlesList.position
}
I know this is not the good way to do it but this is all I have. Any tips? :)
You should put all Vuex state change code into the Vuex store itself, via a mutation or an action. Mutations are for synchronous data changes that will update all reactive components. Actions are for asynchronous events, such as persisting state changes to the server.
So, when the change occurs, it should call a method on your component, which should in turn call the appropriate mutation or action on the Vuex store.
You don't give your full code, but here is a hypothetical example:
(Edited to reflect question about accessing data from within the mutation method.)
Template:
<input type="text" name="position" :value="article.position" #input="updatePosition">
Method on Component:
methods: {
updatePosition(e) {
this.$store.commit('changeArticleOrder', article.position, e.target.value)
}
}
Mutation on Vuex Store:
mutations: {
changeArticleOrder(state, oldPosition, newPosition) {
for (var i = 0; i < 10; i++) {
state.articles[i].position = i + 1;
/* Or do whatever it is you want to do with your positioning. */
}
}

Access component instance from vue watcher

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

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

randomly failing acceptance-tests with Error: Assertion Failed: calling set on destroyed object

Some of my acceptance-tests (which involve actions) fail randomly when run with all other tests but never fail when I run them in isolation.
The Error: Assertion Failed: calling set on destroyed object is triggered in my action
I don't get any issue while using the table as a user so these issues only appear in tests. So I'm not sure if it's just because the app isn't destroyed correctly after each run.
Can I somehow see why the object is about to be destroyed or which observers are blocking it?
foos/index/route.js
startFoo(foos) {
foos.forEach(function(foo) {
foo.set('status', 'active');
foo.save();
});
},
This action gets passed down to a component (table) which shows a list of foo-models. Each row can be selected, doing so add the foo model of this row to a property selectedRows on this table component.
components/my-table/table.js
visibleColumns: Ember.A(),
selectedRows: Ember.A(),
selectRow(row) {
let selectedRows = this.get('selectedRows');
if (selectedRows.contains(row)) {
selectedRows.removeObject(row);
} else {
selectedRows.addObject(row);
}
},
isSelected(row) {
return this.get('selectedRows').contains(row);
},
components/my-table/header/template.hbs
action.cb is the function startFoo
<button {{action action.cb table.selectedRows}}>
{{im-icon icon=action.icon}}
</button>
The table component is pretty modular so it has header, columns, cells, rows and to share the state (like selected rows) I use an Ember.Object which is passed to all parts of the table component (which might be the issue?!!?)
components/my-table/row/component.js
isSelected: computed('table.selectedRows.[]', {
get(/*key*/) {
return this.get('table').isSelected(this.get('content'));
},
set(/*key, value*/) {
this.get('table').selectRow(this.get('content'));
return this.get('table').isSelected(this.get('content'));
},
}),
components/my-table/row/template.hbs
content is a foo model
<td class="shrink">{{input type='checkbox' checked=isSelected}}</td>
{{#each table.visibleColumns as |column|}}
{{my-table/cell content=content column=column}}
{{/each}}
Test
setup and teardown is ember-cli default
moduleForAcceptance('Acceptance | Foos');
test('click on action start changes state to active', function(assert) {
server.create('foo', {
status: 'paused'
});
// This is an ember-page-object
fooPage
.visit()
.selectFoo()
.clickStart();
andThen(function() {
assert.equal(fooPage.foos(1).status(), 'active', 'runs action');
});
});
Checking isDestroyed prior to setting should do the trick
isSelected: computed('table.selectedRows.[]', {
get(/*key*/) {
return this.get('table').isSelected(this.get('content'));
},
set(/*key, value*/) {
if (this.get('isDestroyed')) {
return;
}
this.get('table').selectRow(this.get('content'));
return this.get('table').isSelected(this.get('content'));
},
}),