Reputation: 537
I have two components that aren't children of each other and I simply need to trigger a method/function in the 1st component via a function from the 2nd component.
First component:
// Component - Product Select
app.component('product-select', {
data() {
return {
options: null
}
},
props: {
modelValue: Array,
},
emits: ['update:modelValue'],
template: `
<div class="ui fluid labeled multiple search selection large dropdown">
<input type="hidden"
name="products"
:value="modelValue"
@change="selectProducts">
<i class="dropdown icon"></i>
<div class="default text">
Select Products
</div>
<div class="menu">
<div v-for="(option, index) in options"
class="item"
v-bind:data-value="option.name">
{{ option.name }}
</div>
</div>
</div>
`,
methods: {
// Add the product selections to the modelValue array
selectProducts(event) {
let value = event.target.value.split(',');
this.$emit('update:modelValue', value);
// Properly empty out the modelValue array if no products are selected
if (/^(""|''|)$/.test(value) || value == null) {
this.$emit('update:modelValue', []);
}
},
updateComponent() {
console.log('Component Updated');
}
},
created: function () {
// Build the products array via the products JSON file
fetch(productsJSON)
.then(response => response.json())
.then(options => {
this.options = options;
});
}
});
Second component:
// Component - Product Card
app.component('product-card', {
data() {
return {
selectedSymptoms: []
}
},
props: {
modelValue: Array,
product: String
},
template: `
<div class="ui fluid raised card">
<symptoms-modal v-bind:name="product + ' - Modal'"
v-bind:title="product + ' - Symptoms'"
v-on:child-method="updateParent">
</symptoms-modal>
<div class="content">
<div class="header" @click="removeProduct(product)">
{{ product }}
</div>
</div>
<div v-if="selectedSymptoms.length === 0"
class="content">
<div class="description">
No symptoms selected
</div>
</div>
<div v-for="(symptom, index) in selectedSymptoms"
class="content symptom">
<h4 class="ui left floated header">
{{ symptom }}
<div class="sub header">
Rate your {{ symptom }}
</div>
</h4>
<button class="ui right floated icon button"
v-bind:name="symptom + ' - Remove'"
@click="removeSymptom(symptom)">
<i class="close icon"></i>
</button>
<div class="clear"></div>
<input type="text"
class="js-range-slider"
v-bind:name="product + ' - ' + symptom"
value=""
/>
</div>
<div class="ui bottom attached large button"
@click="openModal">
<i class="add icon"></i>
Add/Remove Symptoms
</div>
</div>
`,
methods: {
openModal() {
// Gets the product name
// Product Name
let product = this.product;
// Builds the modal name
// Product Name - Modal
let modal = product + ' - Modal';
// Gets the modal element
// name="Product Name - Modal"
let target = $('[name="' + modal + '"]');
// Assign the currently selected symptoms to a targettable array
let array = this.selectedSymptoms;
// Opens the appropriate modal
$(target).modal({
closable: false,
// Updates all checkboxes when the modal appears if the user
// removes a symptom from the main screen
onShow: function () {
// For each input
$('input', $(target)).each(function () {
// If it is checked
if ($(this).is(':checked')) {
// If it is a currently selected symptom
if (jQuery.inArray(this.name, array) != -1) {
// Is checked and in array, re-check
$(this).prop('checked', true);
} else {
// Is checked and not in array, un-check
$(this).prop('checked', false);
}
} else {
if (jQuery.inArray(this.name, array) != -1) {
// Is not checked and in array, re-check
$(this).prop('checked', true);
} else {
// Is not checked and not in array, do nothing
}
}
});
},
}).modal('show');
},
updateParent(value_from_child) {
// Update the symptoms values from the modal
this.selectedSymptoms = value_from_child;
},
removeSymptom(symptom) {
this.selectedSymptoms.splice($.inArray(symptom, this.selectedSymptoms), 1);
},
removeProduct(product) {
this.$root.selectedProducts.splice($.inArray(product, this.$root.selectedProducts), 1);
}
},
updated() {
// Add custom range input functionality
$(".js-range-slider").ionRangeSlider({
skin: "round",
grid: false,
min: 1,
max: 5,
from: 2,
step: 1,
hide_min_max: true,
values: [
"1 - Adverse Reaction", "2 - No Change", "3 - Partial Resolution", "4 - Significant Resolution", "5 - Total Resolution"
],
onChange: function (data) {
// Name Attribute
// data.input[0].attributes.name.nodeValue
// Input Value
// data.from_value
}
});
}
});
In the first component, I have the function updateComponent()
which is the one I need to trigger. In the second component, I have the function removeProduct()
which is what needs to trigger the updateComponent()
function.
I've tried using $refs and it didn't work at all, and from my understanding emitting events only works for child > parent components.
Upvotes: 0
Views: 2448
Reputation: 35674
There are several ways to do it, and the implementation depends on you constraints.
First of all, vue3 no longer supports an event bus the way Vue2 did. That means that the event listening outside of what the components bubble up is not a library feature any more. Instead they recommend a 3rd party option
In Vue 3, it is no longer possible to use these APIs to listen to a component's own emitted events from within a component, there is no migration path for that use case.
But the eventHub pattern can be replaced by using an external library implementing the event emitter interface, for example mitt or tiny-emitter.
The bottom line is that it narrows your choice of three "strategies"
watch
(or some part of reactivity) to execute a function.The other part of the equation is the delivery method.
You can setup a file that will hold a singleton reference, which will allow any of the strategies.
create a pubsub instance, then you can listen and emit from anywhere
// pubsub.js
import mitt from 'mitt'
export const emitter = mitt();
Or if you want to just pass the function, you can wrap it, essentially creating a function that holds the instruction to execute another function.
// singleton.js
let updateComponentProxy = () => {};
export const setUpdateComponentProxy = (callback)=>updateComponentProxy=callback;
// component1
created(){
setUpdateComponentProxy(()=>{this.updateComponent();})
}
// component2
// ..on some trigger
updateComponentProxy()
It's a very ugly implementation, but it works, and maybe in some instances that's appropriate
The 3rd option is using the reactivity. You can do this either by using vuex or a diy super paired-down version of it.
// mystore.js
import {ref} from 'vue';
export const updateComponentCount = ref(0);
export const pushUpdateComponentCount = () => updateComponentCount.value++;
// component1
import {watch} from 'vue';
import {updateComponentCount} from 'mystore.js';
created(){
watch(updateComponentCount, this.updateComponent}
}
// component2
import {updateComponentCount} from 'mystore.js';
// ..on some trigger
pushUpdateComponentCount();
This will execute the updateComponent
function when the value of updateComponentCount
changes. You could do a similar thing with vuex
, since it wouldnt (usually) be setup to run a function in the component, but provide some variable on the store that would trigger a change that you'd listen to. Also this example uses a counter, but you could even toggle that between true
and false
because it is not the value that's important it's the mutation.
If you are trying to pass information between non-direct child and parent, but part of the same "ancestry", the provide/inject feature.
This is meant to as a way to pass props without having to hand them along from parent to child, by just having it accessible to any child. You can then use with whichever strategy. There is however a caveat, which is that if you assign it to the root component, it is available to all components, which lends itself to some strategies better than others.
If you assign something to a key on the app.config.globalProperties
object, (where app is the root component instance) you can have that accessible from any of the child components. For example
import mitt from 'mitt';
const app = createApp(App)
app.config.globalProperties.emitter = mitt();
becomes accessible by
// component 1
created(){
this.emitter.on('removeProduct', this.updateComponent())
}
// component 2
removeProduct(product) {
this.$root.selectedProducts.splice($.inArray(product, this.$root.selectedProducts), 1);
this.emitter.emit('removeProduct')
}
If you wanted to use in a vue3 setup()
function though, you will need to access it using getCurrentInstance().appContext.config.globalProperties.emitter
, since the component instance is not in the context.
Upvotes: 2
Reputation: 320
This is a common challenge in Vue, since the framework focuses on parent-to-child and child-to-parent data flows. You basically have two choices:
Have ProductSelect $emit
events up to a common parent, then pass props
down to ProductCard.
Create and import a global event bus.
You might also be interested in Vue's own framework solution:
Upvotes: 1