Vue $emit not works properly on dynamic component /promise - vue.js

I have created a table component which accept dynamic data (th, tr, td,...).
Table data (td) could be a dynamic component as below:
<td>
<component
:is="data.content"
:colspan="data.colspan"
v-bind="data.props"
v-on="data.events"/>
</td>
...
export default {
name: "DynamicTable",
props: {
...
isLoading : { // loading slot will be used if true
type: Boolean,
default: false
}
}
}
I feed required data in another component like this:
<other-html-elements/>
<dynamic-table
:table-heads="tableHeads"
:table-rows="tableRows"
:is-loading="isLoading">
...
computed: { ...
tableRows () {...
new TableData(CancelOrderButton, 'component', {
props: {
order
},
events: {
'updateLoadingStatus': this.updateLoadingStatus
}
})
...
methods: { ...
updateLoadingStatus (status) {
this.isLoading = status
}
and here is my CancelOrderButton:
methods: {
cancelOrder () {
this.$emit('updateLoadingStatus', true)
somePromise().finally(() => {
this.$emit('updateLoadingStatus', false)
})
once I click on a button and invoke the cancelOrder method, the updateLoadingStatus will be emitted without any problem. and after the promise settled, it will be emitted again. but the handler will not triggered.
I have checked everything. I'm sure that events are emitted. this problem will be fixed when I move the second emit statement out of the finally block or I if do not pass isLoading as a props for the dynamicTable.

Try setting the prop for that emit like this:
<dynamic-table
:table-heads="tableHeads"
:table-rows="tableRows"
#update-loading-status="updateLoadingStatus"
:is-loading="isLoading">
And calling that emit like this (although it should work as you have it):
this.$emit('update-loading-status', true)
Also you can define them in a general way and use them in the component you want:
https://v2.vuejs.org/v2/guide/custom-directive.html

Related

How to trigger a 'change' event with parameters?

This is the first time I use the trigger method with maybe needed parameters and it doesn't work.
I use it with method without parameter and no issue :(
I don't understand the options I need to pass to the trigger method: https://vue-test-utils.vuejs.org/api/wrapper/trigger.html
My component:
<template>
...
<v-text-field
:value="item.value"
#change="closeEditMode(item.name, $event)"
data-test-id="edit-field"
/>
...
</template>
<script>
...
vmethods: {
closeEditMode(itemNameToEdit, value) {
if (this.$refs.form.validate()) {
this.$emit('itemChanged', {
name: itemNameToEdit,
value: value,
});
this.fieldToEdit = '';
}
},
}
...
</script>
My test:
...
it('does not render any text fields', async() => {
...
let editableField = wrapper.find('[data-test-id="edit-field"]');
editableField.element.value = 'abc';
editableField.trigger('change'); //here
await wrapper.vm.$nextTick();
editableField = wrapper.find('[data-test-id="edit-field"]');
expect(editableField.exists()).toBeFalsy();
});
...
...
I imagine the issue here is that you're listening for the Vuetify element's change event which hijacks the native change event and only emits the new value.
Since this is a custom event, you need to use wrapper.vm.$emit()
let editableField = wrapper.find('[data-test-id="edit-field"]');
// emit the custom change event
editableField.vm.$emit('change', 'abc')

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.

Vue 2 - change model in main instance from the component

I have the following component:
Vue.component('visible-filter', {
template: `
<span class="text-muted"
#mouseenter="changeClassMouseenter($event)"
#mouseout="changeClassMouseout($event)"
#click="countryTest(filter)"
#clicked="clicked = true"
v-model="clicked"
><slot></slot></span>
`,
props: ['filter', 'clicked'],
methods: {
changeClassMouseenter(event) {
console.log(this.clicked);
event.target.classList.remove('text-muted')
},
changeClassMouseout(event) {
event.target.classList.add('text-muted')
},
countryTest(filter) {
Event.$emit('clicked');
Event.$emit('country-filter', filter);
}
}
});
The Event in the main instance:
Event.$on('clicked', () => {
this.clicked = true;
})
The data in the main instance:
data: {
clicked: false
},
The thing I want to do:
When I click on the element, I want to set the clicked property to true (for that element), and for the rest of the elements I want to set it to false; Also I want to check if the clicked is true/false when the mouseenter/mouseout event is fired.
How can I achive this?
You should not change the parent from child.
To make communication between parent and child (and child->parent as well) you can/should set up the events.
Emit the event in child component. Nice examples here: https://v2.vuejs.org/v2/guide/components.html#Using-v-on-with-Custom-Events
Listen to event in your parent using <child-component #yourEventName='eventHandler(data)'>
Handle data in your parent, so add eventHandler(data) into your methods and do whatever you want with your data.

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

vue-route pass different props to different view component when router changed in vuejs2.0?

In this ticket, we can pass parameters to component when the route changes via
<router-view class="view" :propForA="AData"></router-view>
My question is If we need pass different props to different routed component,
say, for example, propForB property will have BData , propForC property will have CData
how to achieve that? Especially in vuejs2.0?
I would send one prop with different values that fits your component... For example:
<router-view class="view" :propData="propToSend"></router-view>
then something like this
export default {
data: function () {
return {
propAData: {
something: 'value',
somethingElese: ['other', 'value']
},
propBData: {
somethingOther: 123
},
propToSend: null
}
},
watch: {
'$route': function (val, oldVal) {
if (this.$route.name === 'Something') {
this.propToSend = this.propAData
} else {
this.propToSend = this.propBData
}
}
}
}
There are of-course other ways, to achieve same result...