VueJS emit to a function outside VueJS - javascript

I'm trying to emit something from within my VueJS component to a function which sits in the html page containing the component. Am I missing something, or is this not possible?
Within my component as a method:
insert: function(){
this.$emit('insertItem', 123);
}
In the page containing the component:
<medialibrary #insertItem="insertItem(arg);"></medialibrary>
<script>
function insertItem(arg){
console.log('insertItem');
console.log(arg);
}
</script>

This is actually more possible than it seems at first look. If the function is global (in particular, visible to the parent Vue), it can be called by the Vue even if it is not a method of the Vue. (It would arguably be a better idea to create a method that calls the global function.)
The main difficulty with your code was camelCasing where it should be kebab-case.
If you want insertItem to get the arg from the $emit, the HTML should only give the function name, and Vue will take care of passing the args:
<medialibrary id="app" #insert-item="insertItem"></medialibrary>
My snippet uses your original code, which provides arg from the parent Vue.
function insertItem(arg) {
console.log('insertItem');
console.log(arg);
}
new Vue({
el: '#app',
data: {
arg: 'hi there'
},
components: {
medialibrary: {
template: '<div><button #click="insert">Insert</button></div>',
methods: {
insert() {
console.log("Emit");
this.$emit('insert-item', 123);
}
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.2/vue.min.js"></script>
<medialibrary id="app" #insert-item="insertItem(arg);"></medialibrary>

Related

data() variable refuses to update when set inside nested functions

I am very new to VUE so please forgive if my terminology is not correct.
I am trying to set a variable which is defined in the script tag "data()" function.
I am trying to set a new value for a variable defined in data() from inside the "created()" lifecycle event. This works fine if done at the root level, but i need to do it within 2 nested calls as shown below and it will not work when nested inside the function calls:
app.vue
<template>
<div class="container">
<Button #btn-click="providerPicked" id="current-provider" :text="'Current Provider: ' + currentProvider" />
</div>
</template>
<script>
import { ZOHO } from "./assets/ZohoEmbededAppSDK.min.js";
import Button from './components/Button'
export default {
name: 'App',
components: {
Button,
},
data() {
return{
currentProvider: 'x'
}
},
created() {
console.log("CREATED HOOK")
ZOHO.embeddedApp.on("PageLoad",function(data)
{
console.log(data);
//Custom Business logic goes here
let entity = data.Entity;
let recordID = data.EntityId[0];
ZOHO.CRM.API.getRecord({Entity:entity,RecordID:recordID})
.then(function(data){
console.log(data.data[0])
console.log(data.data[0].name)
//THIS DOES NOT WORK - variable still comes back 'x' in template, notice this is nested twice.
this.currentProvider = data.data[0].name;
});
});
ZOHO.embeddedApp.init();
//THIS DOES WORK - SETS VAR TO "reee" in template, notice this is not nested
this.currentProvider = "reee"
}
}
</script>
Use arrow functions instead of anonymous functions.
Arrow functions do not bind this, and thus this will refer to the outer scope.
created() {
console.log("CREATED HOOK")
ZOHO.embeddedApp.on("PageLoad", (data) => {
console.log(data);
//Custom Business logic goes here
let entity = data.Entity;
let recordID = data.EntityId[0];
ZOHO.CRM.API.getRecord({ Entity: entity, RecordID: recordID })
.then((data) => {
console.log(data.data[0])
console.log(data.data[0].name)
//THIS DOES NOT WORK - variable still comes back 'x' in template, notice this is nested twice.
this.currentProvider = data.data[0].name;
});
});
ZOHO.embeddedApp.init();
//THIS DOES WORK - SETS VAR TO "reee" in template, notice this is not nested
this.currentProvider = "reee"
}

VueJs 2.0 emit event from grand child to his grand parent component

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

Vue.js: access global value in template string

I have a very basic Vue.js app that looks like this:
index.html (just the <body> for succinctness)
<div id="root"></div>
main.js
var config = {
title: 'My App',
}
new Vue({
el: '#root',
template: '<div>{{ config.title }}</div>',
})
This gives me:
Property or method "config" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
(found in root instance)
I'm guessing it's because Vue is looking for it in the data store rather than window. Is there some way to signal that config is a global here, or a better way to do this entirely? I suppose I could pass the config object as a prop to my root Vue instance, but it seems like only components accept props. Thanks for any insights on this.
You can try to define your app as follows:
new Vue({
el: '#root',
template: '<div>{{ config.title }}</div>',
data: function() {
return {
config: window.config
}
}
})
In the above definition, your this.config within the root component points to window.config - the global variable.
If you are just trying to pass the App Name in as a hard-coded string you can do something like this:
var nameMyApp = new Vue({
el: '#root',
data: { input: 'My App' },
computed: {
whosapp: function () {
return this.input
}
}
})
With your html like this:
<div id="root">
<div v-html="whosapp"></div>
</div>
Which will pass the value of input to the whosapp div.
But, if you're looking for something more dynamic, this would be the way to go:
With the same html as above...
var nameMyApp = new Vue({
el: '#root',
data: { input: 'My App' },
The initial value is set to My App, which is what will display if not changed through a function call on the method.
methods: {
update: function(name) {
this.input = name;
}
},
Here is where we define a method that, when called, passes in the parameter to update this.input.
computed: {
whosapp: function () {
return this.input
}
}
})
nameMyApp.update("New Name");
And here is where we call the function, giving it the new parameter; making sure that the parameter is a string.
I hope this is what you're looking for.

vue is not defined on the instance but referenced during render

I'm trying to build a simple app in vue and I'm getting an error. My onScroll function behaves as expected, but my sayHello function returns an error when I click my button component
Property or method "sayHello" is not defined on the instance but
referenced during render. Make sure to declare reactive data
properties in the data option. (found in component )
Vue.component('test-item', {
template: '<div><button v-on:click="sayHello()">Hello</button></div>'
});
var app = new Vue({
el: '#app',
data: {
header: {
brightness: 100
}
},
methods: {
sayHello: function() {
console.log('Hello');
},
onScroll: function () {
this.header.brightness = (100 - this.$el.scrollTop / 8);
}
}
});
I feel like the answer is really obvious but I've tried searching and haven't come up with anything. Any help would be appreciated.
Thanks.
But for a few specific circumstances (mainly props) each component is completely isolated from each other. Separate data, variables, functions, etc. This includes their methods.
Thus, test-item has no sayHello method.
You can get rid of the warning by using .mount('#app') after the Vue instance rather than the el attribute.
Check the snippet below;
var app = new Vue({
data: {
header: {
brightness: 100
}
},
methods: {
sayHello: function() {
console.log('Hello');
},
onScroll: function () {
this.header.brightness = (100 - this.$el.scrollTop / 8);
}
}
}).mount('#app');
Please note; the following might not be necessary but did it along the way trying to solve the same issue: Laravel Elixir Vue 2 project.

How can I access a parent method from child component in Vue.js?

I am trying to call a parent/root level method on a child component in Vue.js, but I keep getting a message saying TypeError: this.addStatusClass is not a function.
Vue.component('spmodal', {
props: ['addStatusClass'],
created: function() {
this.getEnvironments();
},
methods: {
getEnvironments: function() {
this.addStatusClass('test');
}
}
});
new Vue({
el: '#app',
methods: {
addStatusClass(data) {
console.log(data);
}
}
});
Here is a full JSBIN example: http://jsbin.com/tomorozonu/edit?js,console,output
If I call this.$parent.addStatusClass('test'); it works fine, but based on the Vue.js documentation, this is bad practice and I should be using props which is not working.
specifying the prop does nothing on its own, you have to actually pass something to it from the parent - in this case, the function.
<spmodal :add-status-class="addStatusClass"></spmodal>

Categories

Resources