Related
Some colleagues of mine have began a fairly complex web application using Vue.js. They would like to be able to use some of the widgets I've made from scratch in the past using JQuery, as re-implementing them would require a large amount of effort/time.
I know that it's possible to safely use JQuery with Vue.js if you are careful, but the information I've been able to find seems relegated to fairly vague blog posts, and my colleagues have notified me that they are struggling to figure out how to do it. So I am considering the possibility that I could find a way that I can nicely wrap my widgets into a portable cross framework library (for starters that can be used in Vue.js). For example, similar to how people create bindings that provide across language APIs. Ideally, it should make it very easy for someone to use it with Vue.js, and should take away the danger of potential pitfalls. Is there any problem with doing this, and is there any existing work that can be leveraged, or idiomatic way that people do this?
For added context, currently, the widget has an interface that includes a constructor (in which you pass the id of a parent DOM element that it will be appended to), a configure function, and it also emits several signals/events when it changes (although, those could be replaced by a function that checks it's state periodically).
As far as creating a portable and cross-framework library is concerned, I would think of jQuery as simply a dependency that allows you create certain elements and perform certain tasks, which you would intercept and/or modify according to the target framework's requirements. So, you are essentially creating a wrapper component around it, as the top 3 JavaScript frameworks (React, Vue, Angular) today are component-based.
One of the key differences (simply put) is: Reactivity system vs. DOM manipulation.
Now, talking about porting a jQuery plugin to Vue — I'm no expert in both libraries but coming from jQuery myself, I'd say it could be as easy as keeping a reference to a widget/plugin instance on a Vue component internal data and/or props and having it optionally expose the corresponding methods. The reason for the methods exposure part being optional is the same reason that characterizes one library from the other—Vue being more versatile as it scales between both a library and a framework.
In jQuery, you would create an instance of an object and pass it around for its public methods usages; whereas in Vue, you don't explicitly create instances except for the root one (you could, but you typically won't have to)—because the component itself is the (internally constructed) instance. And it is the responsibility of a component to maintain its states and data; the sibling and/or parent components will typically have no direct access to them.
Vue and jQuery are similar in that they both support state/data synchronization. With jQuery, it's obvious since all references are in the global scope; with Vue, one would use either v-model or the .sync modifier (replaced with arguments on v-model in Vue 3). Additionally, they also have event subscription with slightly different approaches.
Let's take the jQuery Autocomplete widget and add some Vue support to it. We'll be focusing on 3 things (Options, Events and Methods) and take 3 of their respective items as an example and comparison. I cannot cover everything here, but this should give you some basic ideas.
Setting up: jQuery
For the sake of complying with your specification in question, let's assume this widget/plugin is a new-able class in the window scope.
In jQuery, you would write the following (on document ready or wrapped in IIFE before the closing <body> tag):
var autocomplete = new Autocomplete({
source: [
'vue',
'react',
'angular',
'jquery'
],
appendTo: '#autocomplete-container',
disabled: false,
change: function(event, ui) { },
focus: function(event, ui) { },
select: function(event, ui) { }
});
// And then some other place needing manual triggers on this instance
autocomplete.close();
var isDisabled = autocomplete.option('disabled');
autocomplete.search('ue'); // Matches 'vue' and 'jquery' ;)
With the target element pre-defined or dynamically created somewhere in the parent scope:
<input type="search" class="my-autocomplete" />
Porting to Vue
Since you didn't mention any specific version of Vue in use, I'm going to assume the Macross (latest stable version: 2.6.12, ATTOW) with ES module; otherwise, try the ES modules compatible build.
And for this particular use case in Vue, we want to instantiate this plugin in the mounted hook, because this is where our target element will have been created and available to literally build upon. Learn more on the Lifecycle Hooks in a diagram here.
Creating component: Autocomplete.vue
<template>
<!--
Notice how this `input` element is added right here rather than we requiring
the parent component to add one, because it's now part of the component. :)
-->
<input type="search" class="my-autocomplete" />
</template>
<script>
export default {
// Basically, this is where you define IMMUTABLE "options", so to speak.
props: {
source: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
}
},
// And this is where to prepare and/or specify the internal options of a component.
data: () => ({
instance: null
}),
mounted() {
// `this` here refers to the local Vue instance
this.instance = new Autocomplete({
source: this.source,
disabled: this.disabled,
appendTo: this.$el // Refers to the `input` element on the template,
change: (event, ui) => {
// You can optionally pass anything in the second argument
this.$emit('change', this.instance);
},
focus: (event, ui) => {
this.$emit('focus', this.instance, event);
},
select: (event, ui) => {
this.$emit('select', this, event, ui);
}
});
},
methods: {
close() {
this.instance.autocomplete('close');
},
getOption(optionName) {
return this.instance.autocomplete('option', optionName);
},
search(keyword) {
this.instance.autocomplete('search', keyword);
}
}
}
</script>
Using the component: Parent.vue (or whatever)
<template>
<div class="parent">
<autocomplete
ref="autocomplete"
:source="items"
:disabled="disabled"
#change="onChange"
#focus="onFocus"
#select="onSelect">
</autocomplete>
</div>
</template>
<script>
import Autocomplete from 'path/to/your-components/Autocomplete.vue';
export default {
data: () => ({
items: [
'vue',
'react',
'angular',
'jquery'
],
disabled: false
}),
methods: {
onChange() {
},
onFocus() {
},
onSelect() {
}
},
mounted() {
// Manually invoke a public method as soon as the component is ready
this.$refs.autocomplete.search('ue');
},
components: {
Autocomplete
}
}
</script>
And we're not there just yet! I purposefully left out the "two-way binding" portion of the above example for us to take a closer look at now. However, this step is optional and should only be done if you need to synchronize data/state between the components (parent ↔ child), for example: You have some logic on the component that sets the input's border color to red when certain values get entered. Now, since you are modifying the parent state (say invalid or error) bound to this component as a prop, you need inform them of its changes by $emit-ting the new value.
So, let's make the following changes (on the same Autocomplete.vue component, with everything else omitted for brevity):
{
model: {
prop: 'source',
event: 'modified' // Custom event name
},
async created() {
// An example of fetching remote data and updating the `source` property.
const newSource = await axios.post('api/fetch-data').then(res => res.data);
// Once fetched, update the jQuery-wrapped autocomplete
this.instance.autocomplete('option', 'source', newSource);
// and tell the parent that it has changed
this.$emit('modified', newSource);
},
watch: {
source(newData, oldData) {
this.instance.autocomplete('option', 'source', newData);
}
}
}
We're basically watch-ing "eagerly" for data changes. If preferred, you could do it lazily with the $watch instance method.
Required changes on the parent side:
<template>
<div class="parent">
<autocomplete
ref="autocomplete"
v-model="items"
:disabled="disabled"
#change="onChange"
#focus="onFocus"
#select="onSelect">
</autocomplete>
</div>
</template>
That's going to enable the aforementioned two-way binding. You could do the same with the rest of the props that you need be "reactive", like the disabled prop in this example—only this time you would use .sync modifier; because in Vue 2, multiple v-model isn't supported. (If you haven't got too far though, I'd suggest going for Vue 3 all the way 🙂).
Finally, there are some caveats and common gotchas that you might want to look out for:
Since Vue performs DOM updates asynchronously, it could be processing something that won't take effect until the next event loop "tick", read more on Async Update Queue.
Due to limitations in JavaScript, there are types of changes that Vue cannot detect. However, there are ways to circumvent them to preserve reactivity.
The this object being undefined, null or in unexpected instance when referenced within a nested method or external function. Go to the docs and search for "arrow function" for complete explanation and how to avoid running into this issue.
And we've created ourselves a Vue-ported version of jQuery Autocomplete! And again, those are just some basic ideas to get you started.
Live Demo
const Autocomplete = Vue.extend({
template: `
<div class="autocomplete-wrapper">
<p>{{label}}</p>
<input type="search" class="my-autocomplete" />
</div>
`,
props: {
source: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
},
label: {
type: String
}
},
model: {
prop: 'source',
event: 'modified'
},
data: () => ({
instance: null
}),
mounted() {
const el = this.$el.querySelector('input.my-autocomplete');
this.instance = $(el).autocomplete({
source: this.source,
disabled: this.disabled,
change: (event, ui) => {
// You can optionally pass anything in the second argument
this.$emit('change', this.instance);
},
focus: (event, ui) => {
this.$emit('focus', this.instance, event);
},
select: (event, ui) => {
this.$emit('select', this, event, ui);
}
});
},
methods: {
close() {
this.instance.autocomplete('close');
},
getOption(optionName) {
return this.instance.autocomplete('option', optionName);
},
search(keyword) {
this.instance.autocomplete('search', keyword);
},
disable(toState) {
this.instance.autocomplete('option', 'disabled', toState);
}
},
watch: {
source(newData, oldData) {
this.instance.autocomplete('option', 'source', newData);
},
disabled(newState, oldState) {
this.disable(newState);
}
}
});
new Vue({
el: '#app',
data: () => ({
items: [
'vue',
'react',
'angular',
'jquery'
],
disabled: false
}),
computed: {
computedItems: {
get() {
return this.items.join(', ');
},
set(val) {
this.items = val.split(', ')
}
}
},
methods: {
onChange() {
// Do something
},
onFocus() {},
onSelect(instance, event, ui) {
console.log(`You selected: "${ui.item.value}"`);
}
},
components: {
Autocomplete
}
})
#app {
display: flex;
justify-content: space-between;
}
#app > div {
flex: 0 0 50%;
}
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" />
<link rel="stylesheet" href="/resources/demos/style.css" />
<script src="https://vuejs.org/js/vue.min.js"></script>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<div id="app">
<autocomplete
v-model="items"
:disabled="disabled"
label='Type something (e.g. "ue")'
#change="onChange"
#focus="onFocus"
#select="onSelect">
</autocomplete>
<div>
<p>Edit this comma-separated list of items and see them reflected on the component</p>
<textarea
v-model.lazy="computedItems"
cols="30"
rows="3">
</textarea>
</div>
</div>
P.S. If these widgets are actually in the global window scope and you are using ESLint, you'll want to ensure they are specified as global variables; otherwise, the no-undef rule will warn on variables that are accessed but not defined within the same file. See this post for the solution.
P.P.S. If you need to ship them as a plugin, see: Writing a Plugin (don't worry, there won't be much extra work required).
I'm trying to render a svg file containing Vue.js template syntax at specific places. The file gets rendered correctly.
Upon instantiation of the inner element (the svg), the template syntax gets replaced, but a vue warning is emitted:
vue.js:597 [Vue warn]: Property or method "data" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
In the SVG there is a text containing the template syntax {{data}}
Vue.component('svg-show', {
props: {
model: {
required: true
},
compiled:null
},
render: function (h) {
return h("div",
[
h(this.compiled, {
props: {
data: this.model
}
})
]);
},
created() {
},
mounted() {
console.log("mounted");
console.log(this.model.SVG);
this.compiled = Vue.compile("<div>" + model.SVG + "</div>");
}
});
https://jsfiddle.net/dg2hkeby/10/
i also tried to use a static variable for the data property, but this did not succeed either.
How would one achieve what i'm trying to do using Vue.js?
It seems that Vue.js 2.0 doesn't emit events from a grand child to his grand parent component.
Vue.component('parent', {
template: '<div>I am the parent - {{ action }} <child #eventtriggered="performAction"></child></div>',
data(){
return {
action: 'No action'
}
},
methods: {
performAction() { this.action = 'actionDone' }
}
})
Vue.component('child', {
template: '<div>I am the child <grand-child></grand-child></div>'
})
Vue.component('grand-child', {
template: '<div>I am the grand-child <button #click="doEvent">Do Event</button></div>',
methods: {
doEvent() { this.$emit('eventtriggered') }
}
})
new Vue({
el: '#app'
})
This JsFiddle solves the issue https://jsfiddle.net/y5dvkqbd/4/ , but by emtting two events:
One from grand child to middle component
Then emitting again from middle component to grand parent
Adding this middle event seems repetitive and unneccessary. Is there a way to emit directly to grand parent that I am not aware of?
Vue 2.4 introduced a way to easily pass events up the hierarchy using vm.$listeners
From https://v2.vuejs.org/v2/api/#vm-listeners :
Contains parent-scope v-on event listeners (without .native modifiers). This can be passed down to an inner component via v-on="$listeners" - useful when creating transparent wrapper components.
See the snippet below using v-on="$listeners" in the grand-child component in the child template:
Vue.component('parent', {
template:
'<div>' +
'<p>I am the parent. The value is {{displayValue}}.</p>' +
'<child #toggle-value="toggleValue"></child>' +
'</div>',
data() {
return {
value: false
}
},
methods: {
toggleValue() { this.value = !this.value }
},
computed: {
displayValue() {
return (this.value ? "ON" : "OFF")
}
}
})
Vue.component('child', {
template:
'<div class="child">' +
'<p>I am the child. I\'m just a wrapper providing some UI.</p>' +
'<grand-child v-on="$listeners"></grand-child>' +
'</div>'
})
Vue.component('grand-child', {
template:
'<div class="child">' +
'<p>I am the grand-child: ' +
'<button #click="emitToggleEvent">Toggle the value</button>' +
'</p>' +
'</div>',
methods: {
emitToggleEvent() { this.$emit('toggle-value') }
}
})
new Vue({
el: '#app'
})
.child {
padding: 10px;
border: 1px solid #ddd;
background: #f0f0f0
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<parent></parent>
</div>
NEW ANSWER (Nov-2018 update)
I discovered that we could actually do this by leveraging the $parent property in the grand child component:
this.$parent.$emit("submit", {somekey: somevalue})
Much cleaner and simpler.
The Vue community generally favors using Vuex to solve this kind of issue. Changes are made to Vuex state and the DOM representation just flows from that, eliminating the need for events in many cases.
Barring that, re-emitting would probably be the next best choice, and lastly you might choose to use an event bus as detailed in the other highly voted answer to this question.
The answer below is my original answer to this question and is not an approach I would take now, having more experience with Vue.
This is a case where I might disagree with Vue's design choice and resort to DOM.
In grand-child,
methods: {
doEvent() {
try {
this.$el.dispatchEvent(new Event("eventtriggered"));
} catch (e) {
// handle IE not supporting Event constructor
var evt = document.createEvent("Event");
evt.initEvent("eventtriggered", true, false);
this.$el.dispatchEvent(evt);
}
}
}
and in parent,
mounted(){
this.$el.addEventListener("eventtriggered", () => this.performAction())
}
Otherwise, yes, you have to re-emit, or use a bus.
Note: I added code in the doEvent method to handle IE; that code could be extracted in a reusable way.
Yes, you're correct events only go from child to parent. They don't go further, e.g. from child to grandparent.
The Vue documentation (briefly) addresses this situation in the Non Parent-Child Communication section.
The general idea is that in the grandparent component you create an empty Vue component that is passed from grandparent down to the children and grandchildren via props. The grandparent then listens for events and grandchildren emit events on that "event bus".
Some applications use a global event bus instead of a per-component event bus. Using a global event bus means you will need to have unique event names or namespacing so events don't clash between different components.
Here is an example of how to implement a simple global event bus.
If you want to be flexible and simply broadcast an event to all parents and their parents recursively up to the root, you could do something like:
let vm = this.$parent
while(vm) {
vm.$emit('submit')
vm = vm.$parent
}
Another solution will be on/emit at root node:
Uses vm.$root.$emit in grand-child, then uses vm.$root.$on at the ancestor (or anywhere you'd like).
Updated: sometimes you'd like to disable the listener at some specific situations, use vm.$off (for example: vm.$root.off('event-name') inside lifecycle hook=beforeDestroy).
Vue.component('parent', {
template: '<div><button #click="toggleEventListener()">Listener is {{eventEnable ? "On" : "Off"}}</button>I am the parent - {{ action }} <child #eventtriggered="performAction"></child></div>',
data(){
return {
action: 1,
eventEnable: false
}
},
created: function () {
this.addEventListener()
},
beforeDestroy: function () {
this.removeEventListener()
},
methods: {
performAction() { this.action += 1 },
toggleEventListener: function () {
if (this.eventEnable) {
this.removeEventListener()
} else {
this.addEventListener()
}
},
addEventListener: function () {
this.$root.$on('eventtriggered1', () => {
this.performAction()
})
this.eventEnable = true
},
removeEventListener: function () {
this.$root.$off('eventtriggered1')
this.eventEnable = false
}
}
})
Vue.component('child', {
template: '<div>I am the child <grand-child #eventtriggered="doEvent"></grand-child></div>',
methods: {
doEvent() {
//this.$emit('eventtriggered')
}
}
})
Vue.component('grand-child', {
template: '<div>I am the grand-child <button #click="doEvent">Emit Event</button></div>',
methods: {
doEvent() { this.$root.$emit('eventtriggered1') }
}
})
new Vue({
el: '#app'
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<parent></parent>
</div>
VueJS 2 components have a $parent property that contains their parent component.
That parent component also includes its own $parent property.
Then, accessing the "grandparent" component it's a matter of accessing the "parent's parent" component:
this.$parent["$parent"].$emit("myevent", { data: 123 });
Anyway, this is kinda tricky, and I recommend using a global state manager like Vuex or similar tools, as other responders have said.
I've made a short mixin based on #digout answer. You want to put it, before your Vue instance initialization (new Vue...) to use it globally in project. You can use it similarly to normal event.
Vue.mixin({
methods: {
$propagatedEmit: function (event, payload) {
let vm = this.$parent;
while (vm) {
vm.$emit(event, payload);
vm = vm.$parent;
}
}
}
})
Riffing off #kubaklam and #digout's answers, this is what I use to avoid emitting on every parent component between the grand-child and the (possibly distant) grandparent:
{
methods: {
tunnelEmit (event, ...payload) {
let vm = this
while (vm && !vm.$listeners[event]) {
vm = vm.$parent
}
if (!vm) return console.error(`no target listener for event "${event}"`)
vm.$emit(event, ...payload)
}
}
}
When building out a component with distant grand children where you don't want many/any components to be tied to the store, yet want the root component to act as a store/source of truth, this works quite well. This is similar to the data down actions up philosophy of Ember. Downside is that if you want to listen for that event on every parent in between, then this won't work. But then you can use $propogateEmit as in above answer by #kubaklam.
Edit: initial vm should be set to the component, and not the component's parent. I.e. let vm = this and not let vm = this.$parent
This is the only case when I use event bus!! For passing data from deep nested child, to not directly parent, communication.
First: Create a js file (I name it eventbus.js) with this content:
import Vue from 'vue'
Vue.prototype.$event = new Vue()
Second: In your child component emit an event:
this.$event.$emit('event_name', 'data to pass')
Third: In the parent listen to that event:
this.$event.$on('event_name', (data) => {
console.log(data)
})
Note: If you don't want that event anymore please unregister it:
this.$event.$off('event_name')
INFO: No need to read the below personal opinion
I don't like to use vuex for grand-child to grand-parent communication (Or similar communication level).
In vue.js for passing data from grand-parent to grand-child you can use provide/inject. But there is not something similar for the opposite thing. (grand-child to grand-parent) So I use event bus whenever I have to do that kind of communication.
Riffing off #digout answer. I am thinking that if the purpose is to send data to a far-ancestor then we don't need $emit at all. I did this for my edge-case and it seems to work. Yes, it could be implemented via a mixin but it doesn't have to be.
/**
* Send some content as a "message" to a named ancestor of the component calling this method.
* This is an edge-case method where you need to send a message many levels above the calling component.
* Your target component must have a receiveFromDescendant(content) method and it decides what
* to do with the content it gets.
* #param {string} name - the name of the Vue component eg name: 'myComponentName'
* #param {object} content - the message content
*/
messageNamedAncestor: function (name, content) {
let vm = this.$parent
let found = false
while (vm && !found) {
if (vm.$vnode.tag.indexOf('-' + name) > -1) {
if (vm.receiveFromDescendant) {
found = true
vm.receiveFromDescendant(content)
} else {
throw new Error(`Found the target component named ${name} but you dont have a receiveFromDescendant method there.`)
}
} else {
vm = vm.$parent
}
}
}
Given an ancestor:
export default {
name: 'myGreatAncestor',
...
methods: {
receiveFromDescendant (content) {
console.log(content)
}
}
}
A great grand-child says
// Tell the ancestor component something important
this.messageNamedAncestor('myGreatAncestor', {
importantInformation: 'Hello from your great descendant'
})
As of Vue 3, a number of fundamental changes have happened to root events:
The $on, $off and $once root methods no longer exist. There is to a certain extent something to replace this, since you can listen to root events by doing this:
createApp(App, {
// Listen for the 'expand' event
onExpand() {
console.log('expand')
}
})
Another solution are event buses, but the Vue.js documents take a dim view - they can cause maintenance headaches in the long run. You might get an ever spreading set of emits and event sinks, with no clear or central idea of how it is managed or what components could be affected elsewhere. Nonetheless, examples given by the docs of event buses are mitt and tiny-emitter.
However the docs make it clear that they recommend handling these sorts of situations in this order:
Props A convenient solution for parent / child communications.
Provide/Inject A simple way for ancestors to communicate with their descendants (although critically, not the other way around).
Vuex A way of handling global state in a clear fashion. It's important to note that this is not solely for events, or communications - Vuex was built primarily to handle state.
Essentially the choice for the OP would come down to using an event bus, or Vuex. In order to centralise the event bus, you could place it inside Vuex, if state was also needed to be globally available. Otherwise using an event bus with strict centralised controls on it's behaviour and location might help.
I really dig the way this is handled by creating a class that is bound to the window and simplifying the broadcast/listen setup to work wherever you are in the Vue app.
window.Event = new class {
constructor() {
this.vue = new Vue();
}
fire(event, data = null) {
this.vue.$emit(event, data);
}
listen() {
this.vue.$on(event, callback);
}
}
Now you can just fire / broadcast / whatever from anywhere by calling:
Event.fire('do-the-thing');
...and you can listen in a parent, grandparent, whatever you want by calling:
Event.listen('do-the-thing', () => {
alert('Doing the thing!');
});
It seems that Vue.js 2.0 doesn't emit events from a grand child to his grand parent component.
Vue.component('parent', {
template: '<div>I am the parent - {{ action }} <child #eventtriggered="performAction"></child></div>',
data(){
return {
action: 'No action'
}
},
methods: {
performAction() { this.action = 'actionDone' }
}
})
Vue.component('child', {
template: '<div>I am the child <grand-child></grand-child></div>'
})
Vue.component('grand-child', {
template: '<div>I am the grand-child <button #click="doEvent">Do Event</button></div>',
methods: {
doEvent() { this.$emit('eventtriggered') }
}
})
new Vue({
el: '#app'
})
This JsFiddle solves the issue https://jsfiddle.net/y5dvkqbd/4/ , but by emtting two events:
One from grand child to middle component
Then emitting again from middle component to grand parent
Adding this middle event seems repetitive and unneccessary. Is there a way to emit directly to grand parent that I am not aware of?
Vue 2.4 introduced a way to easily pass events up the hierarchy using vm.$listeners
From https://v2.vuejs.org/v2/api/#vm-listeners :
Contains parent-scope v-on event listeners (without .native modifiers). This can be passed down to an inner component via v-on="$listeners" - useful when creating transparent wrapper components.
See the snippet below using v-on="$listeners" in the grand-child component in the child template:
Vue.component('parent', {
template:
'<div>' +
'<p>I am the parent. The value is {{displayValue}}.</p>' +
'<child #toggle-value="toggleValue"></child>' +
'</div>',
data() {
return {
value: false
}
},
methods: {
toggleValue() { this.value = !this.value }
},
computed: {
displayValue() {
return (this.value ? "ON" : "OFF")
}
}
})
Vue.component('child', {
template:
'<div class="child">' +
'<p>I am the child. I\'m just a wrapper providing some UI.</p>' +
'<grand-child v-on="$listeners"></grand-child>' +
'</div>'
})
Vue.component('grand-child', {
template:
'<div class="child">' +
'<p>I am the grand-child: ' +
'<button #click="emitToggleEvent">Toggle the value</button>' +
'</p>' +
'</div>',
methods: {
emitToggleEvent() { this.$emit('toggle-value') }
}
})
new Vue({
el: '#app'
})
.child {
padding: 10px;
border: 1px solid #ddd;
background: #f0f0f0
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<parent></parent>
</div>
NEW ANSWER (Nov-2018 update)
I discovered that we could actually do this by leveraging the $parent property in the grand child component:
this.$parent.$emit("submit", {somekey: somevalue})
Much cleaner and simpler.
The Vue community generally favors using Vuex to solve this kind of issue. Changes are made to Vuex state and the DOM representation just flows from that, eliminating the need for events in many cases.
Barring that, re-emitting would probably be the next best choice, and lastly you might choose to use an event bus as detailed in the other highly voted answer to this question.
The answer below is my original answer to this question and is not an approach I would take now, having more experience with Vue.
This is a case where I might disagree with Vue's design choice and resort to DOM.
In grand-child,
methods: {
doEvent() {
try {
this.$el.dispatchEvent(new Event("eventtriggered"));
} catch (e) {
// handle IE not supporting Event constructor
var evt = document.createEvent("Event");
evt.initEvent("eventtriggered", true, false);
this.$el.dispatchEvent(evt);
}
}
}
and in parent,
mounted(){
this.$el.addEventListener("eventtriggered", () => this.performAction())
}
Otherwise, yes, you have to re-emit, or use a bus.
Note: I added code in the doEvent method to handle IE; that code could be extracted in a reusable way.
Yes, you're correct events only go from child to parent. They don't go further, e.g. from child to grandparent.
The Vue documentation (briefly) addresses this situation in the Non Parent-Child Communication section.
The general idea is that in the grandparent component you create an empty Vue component that is passed from grandparent down to the children and grandchildren via props. The grandparent then listens for events and grandchildren emit events on that "event bus".
Some applications use a global event bus instead of a per-component event bus. Using a global event bus means you will need to have unique event names or namespacing so events don't clash between different components.
Here is an example of how to implement a simple global event bus.
If you want to be flexible and simply broadcast an event to all parents and their parents recursively up to the root, you could do something like:
let vm = this.$parent
while(vm) {
vm.$emit('submit')
vm = vm.$parent
}
Another solution will be on/emit at root node:
Uses vm.$root.$emit in grand-child, then uses vm.$root.$on at the ancestor (or anywhere you'd like).
Updated: sometimes you'd like to disable the listener at some specific situations, use vm.$off (for example: vm.$root.off('event-name') inside lifecycle hook=beforeDestroy).
Vue.component('parent', {
template: '<div><button #click="toggleEventListener()">Listener is {{eventEnable ? "On" : "Off"}}</button>I am the parent - {{ action }} <child #eventtriggered="performAction"></child></div>',
data(){
return {
action: 1,
eventEnable: false
}
},
created: function () {
this.addEventListener()
},
beforeDestroy: function () {
this.removeEventListener()
},
methods: {
performAction() { this.action += 1 },
toggleEventListener: function () {
if (this.eventEnable) {
this.removeEventListener()
} else {
this.addEventListener()
}
},
addEventListener: function () {
this.$root.$on('eventtriggered1', () => {
this.performAction()
})
this.eventEnable = true
},
removeEventListener: function () {
this.$root.$off('eventtriggered1')
this.eventEnable = false
}
}
})
Vue.component('child', {
template: '<div>I am the child <grand-child #eventtriggered="doEvent"></grand-child></div>',
methods: {
doEvent() {
//this.$emit('eventtriggered')
}
}
})
Vue.component('grand-child', {
template: '<div>I am the grand-child <button #click="doEvent">Emit Event</button></div>',
methods: {
doEvent() { this.$root.$emit('eventtriggered1') }
}
})
new Vue({
el: '#app'
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<parent></parent>
</div>
VueJS 2 components have a $parent property that contains their parent component.
That parent component also includes its own $parent property.
Then, accessing the "grandparent" component it's a matter of accessing the "parent's parent" component:
this.$parent["$parent"].$emit("myevent", { data: 123 });
Anyway, this is kinda tricky, and I recommend using a global state manager like Vuex or similar tools, as other responders have said.
I've made a short mixin based on #digout answer. You want to put it, before your Vue instance initialization (new Vue...) to use it globally in project. You can use it similarly to normal event.
Vue.mixin({
methods: {
$propagatedEmit: function (event, payload) {
let vm = this.$parent;
while (vm) {
vm.$emit(event, payload);
vm = vm.$parent;
}
}
}
})
Riffing off #kubaklam and #digout's answers, this is what I use to avoid emitting on every parent component between the grand-child and the (possibly distant) grandparent:
{
methods: {
tunnelEmit (event, ...payload) {
let vm = this
while (vm && !vm.$listeners[event]) {
vm = vm.$parent
}
if (!vm) return console.error(`no target listener for event "${event}"`)
vm.$emit(event, ...payload)
}
}
}
When building out a component with distant grand children where you don't want many/any components to be tied to the store, yet want the root component to act as a store/source of truth, this works quite well. This is similar to the data down actions up philosophy of Ember. Downside is that if you want to listen for that event on every parent in between, then this won't work. But then you can use $propogateEmit as in above answer by #kubaklam.
Edit: initial vm should be set to the component, and not the component's parent. I.e. let vm = this and not let vm = this.$parent
This is the only case when I use event bus!! For passing data from deep nested child, to not directly parent, communication.
First: Create a js file (I name it eventbus.js) with this content:
import Vue from 'vue'
Vue.prototype.$event = new Vue()
Second: In your child component emit an event:
this.$event.$emit('event_name', 'data to pass')
Third: In the parent listen to that event:
this.$event.$on('event_name', (data) => {
console.log(data)
})
Note: If you don't want that event anymore please unregister it:
this.$event.$off('event_name')
INFO: No need to read the below personal opinion
I don't like to use vuex for grand-child to grand-parent communication (Or similar communication level).
In vue.js for passing data from grand-parent to grand-child you can use provide/inject. But there is not something similar for the opposite thing. (grand-child to grand-parent) So I use event bus whenever I have to do that kind of communication.
Riffing off #digout answer. I am thinking that if the purpose is to send data to a far-ancestor then we don't need $emit at all. I did this for my edge-case and it seems to work. Yes, it could be implemented via a mixin but it doesn't have to be.
/**
* Send some content as a "message" to a named ancestor of the component calling this method.
* This is an edge-case method where you need to send a message many levels above the calling component.
* Your target component must have a receiveFromDescendant(content) method and it decides what
* to do with the content it gets.
* #param {string} name - the name of the Vue component eg name: 'myComponentName'
* #param {object} content - the message content
*/
messageNamedAncestor: function (name, content) {
let vm = this.$parent
let found = false
while (vm && !found) {
if (vm.$vnode.tag.indexOf('-' + name) > -1) {
if (vm.receiveFromDescendant) {
found = true
vm.receiveFromDescendant(content)
} else {
throw new Error(`Found the target component named ${name} but you dont have a receiveFromDescendant method there.`)
}
} else {
vm = vm.$parent
}
}
}
Given an ancestor:
export default {
name: 'myGreatAncestor',
...
methods: {
receiveFromDescendant (content) {
console.log(content)
}
}
}
A great grand-child says
// Tell the ancestor component something important
this.messageNamedAncestor('myGreatAncestor', {
importantInformation: 'Hello from your great descendant'
})
As of Vue 3, a number of fundamental changes have happened to root events:
The $on, $off and $once root methods no longer exist. There is to a certain extent something to replace this, since you can listen to root events by doing this:
createApp(App, {
// Listen for the 'expand' event
onExpand() {
console.log('expand')
}
})
Another solution are event buses, but the Vue.js documents take a dim view - they can cause maintenance headaches in the long run. You might get an ever spreading set of emits and event sinks, with no clear or central idea of how it is managed or what components could be affected elsewhere. Nonetheless, examples given by the docs of event buses are mitt and tiny-emitter.
However the docs make it clear that they recommend handling these sorts of situations in this order:
Props A convenient solution for parent / child communications.
Provide/Inject A simple way for ancestors to communicate with their descendants (although critically, not the other way around).
Vuex A way of handling global state in a clear fashion. It's important to note that this is not solely for events, or communications - Vuex was built primarily to handle state.
Essentially the choice for the OP would come down to using an event bus, or Vuex. In order to centralise the event bus, you could place it inside Vuex, if state was also needed to be globally available. Otherwise using an event bus with strict centralised controls on it's behaviour and location might help.
I really dig the way this is handled by creating a class that is bound to the window and simplifying the broadcast/listen setup to work wherever you are in the Vue app.
window.Event = new class {
constructor() {
this.vue = new Vue();
}
fire(event, data = null) {
this.vue.$emit(event, data);
}
listen() {
this.vue.$on(event, callback);
}
}
Now you can just fire / broadcast / whatever from anywhere by calling:
Event.fire('do-the-thing');
...and you can listen in a parent, grandparent, whatever you want by calling:
Event.listen('do-the-thing', () => {
alert('Doing the thing!');
});
Let's say I have a basic page with VueJS as follows:
Vue.component('child', {
template: '<p>Placed at index {{index}}</p>',
data() {
return {
index: 0
}
},
mounted() {
this.index = this.$parent.addElement(this);
}
});
new Vue({
el: '#theParent',
data() {
return {
allElements: []
}
},
methods: {
addElement(elem) {
this.allElements.push(elem);
return this.allElements.length - 1;
}
}
});
<script src="https://cdn.jsdelivr.net/vue/2.3.2/vue.min.js"></script>
<div id="theParent">
<child></child>
<child></child>
<child></child>
</div>
The purpose of the output is just to illustrate at what index the elements have been inserted at. My use case requires that the elements are added in the same order that they appear in the HTML. Every time I run this page it appears that this is indeed happening as the output is in order.
My question is: Is this behavior guaranteed to always happen - will VueJS always execute mounted() on components in the order they appear in the HTML? If not, is there an alternate way to guarantee that they are added to my array in the proper order?
from my experience with Vue and Ag-Grid extension, it's not always the case.
To make sure it's loaded in the correct order, I implement watch function to let Vue know the DOM is ready.
even with beforeMount or created wont't help if the DOM element is somehow late-triggered.
computed: {
elementWatcher () {
return this.whateverDOMElement // after this child is ready
}
}
watch: {
elementWatcher (val) {
if (val) {
this.mountAnother() // parent can be mounted
}
}
}