I have a button in a Vue component
<el-button class='range-right' #click='deleteItem(item)'>delete</el-button>
within it's handler I want to invoke other methods of the component.
however, even though I can call deleteItem which is itself a component method, I cannot get at the actual component for other methods.
is there a way to pass something like a $component param to the #click event?
methods {
deleteItem: (item, obj) => {
let api = '/api/train/deleteItem?_id=' + item._id
let resource = Vue.resource(api)
let vm = this // NOT a component
window.delItem = this
console.log('deleteItem.this', this)
resource.delete(api)
.then(items => {
console.log('deleted')
vm.methods.load() //<< fails
})
},
I think the problem is the arrow function as commented
See an example here using function:
https://jsfiddle.net/rvp6fvwp/
Arrow function is bound to the parent context as said here https://v2.vuejs.org/v2/guide/instance.html#Properties-and-Methods
Don’t use arrow functions on an instance property or callback (e.g.
vm.$watch('a', newVal => this.myMethod())). As arrow functions are
bound to the parent context, this will not be the Vue instance as
you’d expect and this.myMethod will be undefined.
Related
I created the child using:
const ComponentClass = Vue.extend(someComponent);
const instance = new ComponentClass({
propsData: { prop: this.value }
})
instance.$mount();
this.$refs.container.appendChild(instance.$el);
When this.value is updated in the parent, its value doesn't change in the child. I've tried to watch it but it didn't work.
Update:
There's an easier way to achieve this:
create a <div>
append it to your $refs.container
create a new Vue instance and .$mount() it in the div
set the div instance's data to whatever you want to bind dynamically, getting values from the parent
provide the props to the mounted component from the div's data, through render function
methods: {
addComponent() {
const div = document.createElement("div");
this.$refs.container.appendChild(div);
new Vue({
components: { Test },
data: this.$data,
render: h => h("test", {
props: {
message: this.msg
}
})
}).$mount(div);
}
}
Important note: this in this.$data refers the parent (the component which has the addComponent method), while this inside render refers new Vue()'s instance. So, the chain of reactivity is: parent.$data > new Vue().$data > new Vue().render => Test.props. I had numerous attempts at bypassing the new Vue() step and passing a Test component directly, but haven't found a way yet. I'm pretty sure it's possible, though, although the solution above achieves it in practice, because the <div> in which new Vue() renders gets replaced by its template, which is the Test component. So, in practice, Test is a direct ancestor of $refs.container. But in reality, it passes through an extra instance of Vue, used for binding.
Obviously, if you don't want to add a new child component to the container each time the method is called, you can ditch the div placeholder and simply .$mount(this.$refs.container), but by doing so you will replace the existing child each subsequent time you call the method.
See it working here: https://codesandbox.io/s/nifty-dhawan-9ed2l?file=/src/components/HelloWorld.vue
However, unlike the method below, you can't override data props of the child with values from parent dynamically. But, if you think about it, that's the way data should work, so just use props for whatever you want bound.
Initial answer:
Here's a function I've used over multiple projects, mostly for creating programmatic components for mapbox popups and markers, but also useful for creating components without actually adding them to DOM, for various purposes.
import Vue from "vue";
// import store from "./store";
export function addProgrammaticComponent(parent, component, dataFn, componentOptions) {
const ComponentClass = Vue.extend(component);
const initData = dataFn() || {};
const data = {};
const propsData = {};
const propKeys = Object.keys(ComponentClass.options.props || {});
Object.keys(initData).forEach(key => {
if (propKeys.includes(key)) {
propsData[key] = initData[key];
} else {
data[key] = initData[key];
}
});
const instance = new ComponentClass({
// store,
data,
propsData,
...componentOptions
});
instance.$mount(document.createElement("div"));
const dataSetter = data => {
Object.keys(data).forEach(key => {
instance[key] = data[key];
});
};
const unwatch = parent.$watch(dataFn || {}, dataSetter);
return {
instance,
update: () => dataSetter(dataFn ? dataFn() : {}),
dispose: () => {
unwatch();
instance.$destroy();
}
};
}
componentOptions is to provide any custom (one-off) functionality to the new instance (i.e.: mounted(), watchers, computed, store, you name it...).
I've set up a demo here: https://codesandbox.io/s/gifted-mestorf-297xx?file=/src/components/HelloWorld.vue
Notice I'm not doing the appendChild in the function purposefully, as in some cases I want to use the instance without adding it to DOM. The regular usage is:
const component = addProgrammaticComponent(this, SomeComponent, dataFn);
this.$el.appendChild(component.instance.$el);
Depending on what your dynamic component does, you might want to call .dispose() on it in parent's beforeDestroy(). If you don't, beforeDestroy() on child never gets called.
Probably the coolest part about it all is you don't actually need to append the child to the parent's DOM (it can be placed anywhere in DOM and the child will still respond to any changes of the parent, like it would if it was an actual descendant). Their "link" is programmatic, through dataFn.
Obviously, this opens the door to a bunch of potential problems, especially around destroying the parent without destroying the child. So you need be very careful and thorough about this type of cleanup. You either register each dynamic component into a property of the parent and .dispose() all of them in the parent's beforeDestroy() or give them a particular selector and sweep the entire DOM clean before destroying the parent.
Another interesting note is that in Vue 3 all of the above will no longer be necessary, as most of the core Vue functionality (reactivity, computed, hooks, listeners) is now exposed and reusable as is, so you won't have to $mount a component in order to have access to its "magic".
My method in vue looks like this :
methods: {
setDate: async function () {
console.log(this.modal)
}
}
I want to change it to an arrow function. I tried like this :
methods: {
setDate: async () => {
console.log(this.modal)
}
}
There exist error like this :
Cannot read property 'modal' of undefined
How can I solve this problem?
use function directly like
methods: {
async setDate() {
console.log(this.modal)
}
}
You are facing this error because an arrow function wouldn't bind this to the vue instance for which you are defining the method. The same would happen if you were to define computed properties using an arrow function.
Don’t use arrow functions on an instance property or callback e.g.
vm.$watch('a', newVal => this.myMethod())
As arrow functions are bound to the parent context, this will not be the Vue instance as you’d expect and this.myMethod will be undefined.
You can read about it here.
This link https://michaelnthiessen.com/this-is-undefined/ says the following:
"An arrow function uses what is called lexical scoping. We'll get into this more in a bit, but it basically means that the arrow function takes this from it's context.
If you try to access this from inside of an arrow function that's on a Vue component, you'll get an error because this doesn't exist!
So in short, try to avoid using arrow functions on Vue components. It will save you a lot of headaches and confusion."
According to the page on event handling in the docs for Vue, when you use v-on like v-on:click="handler" the handler function will automatically get the original DOM event as the first argument. This code snippet is directly adapted from those docs.
new Vue({
// Vue config shortened for brevity
methods: {
handler(event) {
// `this` inside methods points to the Vue instance
alert('Hello ' + this.name + '!')
// `event` is the native DOM event
if (event) {
alert(event.target.tagName)
}
}
}
})
Why the heck can I still access event even if I omit it from the functions parameter list like this:
handler() {
console.log(event); // Still returns the native DOM object even though
// I don't explicitly define `event` anywhere
}
Shouldn't event be undefined if I don't add it as an argument to the function?
I believe that'll be the global window.event:
https://developer.mozilla.org/en-US/docs/Web/API/Window/event
Nothing to do with Vue, it's just an unfortunate coincidence that you happened to call it event.
Maybe the docs explains the reason to use event in the handler function as first argument:
You should avoid using this property in new code, and should instead use the Event passed into the event handler function.
https://developer.mozilla.org/en-US/docs/Web/API/Window/event
I have this following vuejs component hierarchy.
What i want to do it to invoke COMP_B_ONE validate() method, when COMP_A_TWO submit() method is invoked EVERY TIME.
MAIN_COMPONENT
COMP_A_ONE
COMP_B_ONE
validate()
COMP_B_TWO
validate()
COMP_A_TWO
submit()
I've already implemented an emit when submit is triggered in COMP_A_TWO which can be listened in MAIN_COMPONENT
submit() {
this.$emit('submit')
}
what seems to be the best approach in this regard? any suggestions appreciated.
I can get this done by two ways.
1 - Global EventBus
I will create an eventBus and register events on it from any file and listen it anywhere -
import { EventBus } from '#/eventBus'
// simply import it to component which need listen the event
//Register Event where you have your methods - like In your COMP_B_TWO
EventBus.$on('validate', () => { this.validate() })
// Emit event from another component
EventBus.$emit('validate')// Like directly from your COMP_A_TWO
To know how to create a eventBus follow this - Global Event Bus Vue
Another way I can think is
2 - Refs
Add reference to COMP_A_ONE like
<COMP_A_ONE ref = "one" />
Then add reference to COMP_B_ONE
<COMP_B_ONE ref = "b-one" />
Now when you trigger submit from main component
execute it -
this.$on('submit', () => {
this.$refs.one['b-one'].validate()
})
It totally depends which way you wanna go -
If you need to call validate for many more places, I would suggest choosing EventBus
You just need current component to have it, use Refs
I'm using ElementUi NavMenu, and render function with the createElement method to make the items of the menu just using JSON with titles and index of the menu, just HTML and JS files, not .vue files.
The menu is mounted, the submenus are shown when I click it, but the actions of the submenu (el-menu-item) does not work. I even try the attributes click, item-click, v-on: click when creating the-menu-item (the documentation of ElementUi tells that #click must be used, but this causes an error on createElement when the attributes are defined), but no one works, no error occurs, as if the method was not been declared.
Only onclick attribute works on the el-menu-item, but when I use it, the method of vue component is not called, and so I have to make a function outside of component (on a class for example), and when this function is called it performs a call to component method (I try $ emits) and an error occurs, because the method of component is not found.
How can I add #click (or similar) event on the el-menu-item inside render function of the component to call a method of the same component?
Documenation of NavMenu of ElementUI.
How I'm creating menu item:
createElement("el-menu-item",{
attrs:{
index:json[i].id,
click:json[i].onclick
}},
json[i].title
)
Actually, this is mentioned in Vue.js documentation.
See https://v2.vuejs.org/v2/guide/render-function.html#The-Data-Object-In-Depth .
e.g. https://codepen.io/jacobgoh101/pen/ypjGqw?editors=0010
Vue.component("test", {
render: function(createElement) {
return createElement(
"button",
{
on: {
click: function() {
alert('click');
}
}
},
"Header"
);
}
});