How can I call a method in VueJS when some html elements are mounted?
For example:
...
data: [
{
items: [
{text:'item1'},
{text:'item2'}
]
}
]
methods: function() {
myMethod: function(a) {
console.warn(a)
}
}
template: '<div
v-for="item in items"
:key="item.text"
#hook:mounted="myMethod(item.text + \" mounted\")"
>{{ item.text }}</div>'
...
I need to get HTML like this:
<div>item1</div>
<div>item2</div>
And I need to get 2 warn messages in console:
item1 mounted
item2 mounted
Or I can do it with another way?
Well, the #hook:mounted syntax adds a life-cycle hook to a Vue Component.
<div></div> is a plain HTML element.. you need to add a mounted() method to your component options. The reason #click works and #hook doesn't is because HTML elements have click events; they don't have Vue component lifecycle hooks.
EDIT: To demonstrate, go ahead and visit this Codesandbox, navigate to App.vue and try to move the #hook:mounted directive from the Child component to the div that contains it, you'll notice the hook is never called because div has no life-cycle hooks
EDIT: Also your Vue component needs to look like this:
mounted: function(a) {
this.myMethod("Hello, I am mounted!");
},
methods: {
myMethod(a) { console.warn(a); }
},
template: '<div></div>'
The only way to simulate a component mounting on divs is to actually create a component that is a div:
V-Div.vue
<template>
<div><slot></slot></div>
</template>
You need a <slot> so that you can pass content into the <v-div>
Include V-Div in your component (the component you created, not V-Div.vue)
import VDiv from './components/V-Div.vue';
// ...
{
data() { return { /* data */} },
computed: {},
// other options....
components: {VDiv}
}
Use the VDiv in your component template:
<v-div #hook:mounted="myMethod(item.text + \" mounted\")">{{item.text}}</v-div>
EDIT: I would not recommend doing this, turning a plain HTML Element into a Vue component just to subscribe to life-cycles is a waste of resources.
As others have noted, the hook:mounted event is only available for components and won't fire for HTML elements.
You can do something similar for elements using a custom directive. While an element can't be 'mounted' we can nevertheless have a directive that is called when the element is available.
new Vue({
el: '#app',
directives: {
myMountedDirective (el, { value }) {
console.warn(value + ' mounted')
}
},
data () {
return {
items: [
{text: 'item1'},
{text: 'item2'}
]
}
}
})
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="app">
<div
v-for="item in items"
:key="item.text"
v-my-mounted-directive="item.text"
>
{{ item.text }}
</div>
</div>
However, all of this is very DOM-driven. Custom directives exist so that tweaks can be made to the DOM nodes after they exist. Generally that isn't the correct way to solve the underlying problem.
Usually it is better to make decisions based on the data rather than the DOM. So rather than watching for changes to the DOM we can watch for changes in items.
If you really do need access to the DOM nodes (e.g. to measure sizes) then you might be better off using refs. You can add a ref attribute to each <div> and they will then be accessible as an array via $refs. The mounted and updated hooks of the surrounding component are generally the appropriate places to react to changes in the $refs as they all called immediately after rendering.
Related
I have a component which should pass everything on to the child. I'm successfully passing $attrs and $listeners already:
<template>
<el-form v-on="$listeners" v-bind="$attrs" :label-position="labelPosition">
<slot />
</el-form>
</template>
But I'm unsure how to also forward $refs like we can do in React, so that when using my component like this:
<el-form-responsive
class="form"
:model="formValues"
status-icon
:rules="rules"
ref="form"
label-width="auto"
#submit.native.prevent="submitForm"
>
Then this.$refs.form is actually a reference to the child <el-form>.
I would rather do this transparently, as in you pass exactly the same props to el-form-responsive as you would to a el-form without needing to know that refs has to be passed in a special way.
I don't think it is possible to directly mimic React's ref. A ref attribute in Vue is just a string which is used to register a child component reference to the parent's $refs object during the render function.
Here are the links to documentations doc & doc
So basically it's a kind of inverted logic.. instead of passing a ref to a child in Vue we get it from the child into the parent. So it's not really possible at this point to create a grandchild reference, which is what you need.
There are some workarounds though.
1. Quick dirty and not transparent but technically it would work:
In the parent component, which uses your el-form-responsive, on mounted hook we could replace the original child reference with the grandchild ref.
Your el-form-responsive component. Template:
<el-form ref="elform">
A parent which uses your el-form-responsive. Template:
<el-form-responsive ref="form">
Script:
...
mounted () {
this.$refs.form = this.$refs.form.$refs.elform
}
And after this this.$refs.form is actually a reference to the granchild <el-form>
2. This one would be more elaborate, but probably mach better then the first method:
In order to make the el-form-responsive component really transparent you could expose some of the methods and properties from the child el-form component to any potential parent. Something like this:
el-form-responsive. Template:
<el-form ref="elform">
Script:
export default {
data: () => ({
whatever: null
}),
mounted () {
this.whatever = this.$refs.elform.whatever
},
methods: {
submit () {
this.$refs.elform.submit()
}
}
}
So then inside some parent el-form-responsive could be used like this:
<el-form-responsive ref="form">
...
mounted () {
const formWhatever = this.$refs.form.whatever // actually `whatever` from `el-form`
this.$refs.form.submit() // eventually calls submit on `el-form`
},
If you are working with a custom component with the script setup in Vue 3, it's worth nothing that template refs work like this.
Essentially, you will have to use defineExpose to "expose" your child component data to the parent component
Try this to replace parent's ref with child's , In el-form-responsive
<template>
<el-form v-on="$listeners" v-bind="$attrs" :label-position="labelPosition" ref='ref'>
<slot />
</el-form>
</template>
mounted () {
Object.entries(this.$parent.$refs).forEach(([key, value]) => {
if (value === this) {
this.$parent.$refs[key] = this.$refs.ref
}
})
...
It is work for me:
mounted() {
Object.assign(Object.getPrototypeOf(this), this.$refs.formRef)
}
warning: insert to __proto__,maybe is bad way
I have a giant, dynamic HTML string that I'm loading into a div within a Vue component. The HTML string is essentially the content from a WYSIWYG editor. Originally, I was just using v-html for this, and it was fine.
However, there are now cases where I need to replace part of the HTML string with an actual Vue component, and I'm not sure of the best way to do that.
As an example, I might have some markup in the HTML string that looks like the following:
||map:23||
And what I want to do is replace that with a Vue component like the following:
<map-component :id="23"></map-component>
I tried doing the string conversion ahead of time in Laravel and then just using v-html in the Vue component to inject the content, but that doesn't seem to load the Vue component.
I then tried using a slot for the HTML content, and that does work, but it has the nasty side effect of showing a bunch of unformatted HTML content on the screen for a second or two before Vue is able to properly render it.
So my question is: Is there another (more elegant) way to do this? I was thinking that after the Vue component loads with the HTML content, I could somehow find the, for example, ||map:23|| instances in the markup and then dynamically replace them with the correct Vue component, but if that's possible, I don't know how; I couldn't find anything in the Vue docs.
Does anyone know if this is possible? Thank you.
You can use Vue.compile to compile a template string (that can include vue components).
Then you can combine this with a component that has a render() method, to just render the template:
// this component will dynamically compile the html
// and use it as template for this component.
Vue.component("dynamic-html", {
props: ["html"],
computed: {
template() {
if(this.html)
return Vue.compile(this.html).render;
return null;
}
},
render() {
if(this.template)
return this.template();
return null;
}
});
This allows you to render arbirary template strings, which can also contain vue components:
<dynamic-html html="<some-component></some-component>">
</dynamic-html>
Additionally, you can also use this to pass down props / event handlers to components within your string:
<!-- Passing down props -->
<dynamic-html
html='<some-component :prop="$attrs.myprop"></some-component>'
:myprop="12"
></dynamic-html>
<!-- passing down events -->
<dynamic-html
html='<some-component #click="$emit('foo', $event)"></some-component>'
#foo="doSomething"
></dynamic-html>
(you need to use $attrs though to access the props, because they're not in the props definition of the dynamic-html component)
Full code example:
// this component will dynamically compile the html
// into a vue component
Vue.component("dynamic-html", {
props: ["html"],
computed: {
template() {
if(this.html)
return Vue.compile(this.html).render;
return null;
}
},
render() {
if(this.template)
return this.template();
return null;
}
});
Vue.component("red-bold-text", {
props: ["text"],
template: '<span class="red">{{text}}</span>'
});
new Vue({
el: '#root',
data: {
html: null,
myBoundVar: "this is bound from the parent component"
},
mounted() {
// get the html from somewhere...
setTimeout(() => {
this.html = `
<div>
WELCOME!
<red-bold-text text="awesome text"></red-bold-text>
<red-bold-text :text="$attrs.bound"></red-bold-text>
<button #click="$emit('buttonclick', $event)">CLICK ME</button>
</div>
`;
}, 1000);
},
methods: {
onClick(ev) {
console.log("You clicked me!");
}
}
});
.red { color: red; font-weight: bold; margin: 6px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="root">
<div>This will load dynamically:</div>
<dynamic-html :html="html" :bound="myBoundVar" #buttonclick="onClick"></dynamic-html>
</div>
Turtlefight's answer is very helpful and complete, but for anyone looking for a quick, simple answer, use the literal component component with :is as follows to inject HTML content containing Vue components into a dynamic component:
// htmlStrWithVueComponents is a large string containing HTML and Vue components.
<component
:is="{
template: `<div>${htmlStrWithVueComponents}</div>`
}"
>
</component>
Here're a couple of sources that describe this technique:
https://jannerantala.com/tutorials/vue-replace-text-with-component-with-props/
https://jsfiddle.net/Herteby/5kucj6ht/
Edit: It's worth noting that component :is is fairly limited in what you can do. You can't make very complex template strings or added mounted methods, etc.
For my particular use case, because I needed some of this more complex stuff, I ended up going with the following, which is kind of a hybrid between the simpler answer above and Turtlefight's answer:
// This code goes within the parent component's mounted method or wherever is makes sense:
Vue.component('component-name-here', {
// Can add much more complex template strings here.
template: `
<div class="someClass">
${content}
</div>
`,
// Can add lifecycle hooks, methods, computed properties, etc.
mounted: () => {
// Code here
}
});
const res = Vue.compile(`
<component-name-here>
</component-name-here>
`);
new Vue({
render: res.render,
staticRenderFns: res.staticRenderFns
}).$mount('dom-selector-for-dom-element-to-be-replaced-by-vue-component');
It is mentioned in multiple places that mutating properties is an anti-pattern in vue.js. It warns the user not to do it, even when mutating object properties or using functions like this.$set to add elements to an object given as a property.
Given that, what is supposed to be the correct way of having a component responsible for modifying some data it's given? The vue.js suggested pattern seems to be emitting events, but consider this fictional example:
// Main Component
<template>
// content skipped for simplicity
<some-component :someObject="something" />
</template>
// SomeComponent
// Passes this data to another component
<template>
<another-component :someObject="someObject" />
</template>
// AnotherComponent
// Does things using someObjet
Let's imagine that the third component, AnotherComponent is responsible for displaying some data that has been passed down all the way from the main UI that used a component that ended up using this one. And let's say this AnotherComponent maybe can even pass this data (or even some of it) to other components.
Does it mean that then, the component that will end up modifying this data (for example a delete button in a list of things) has to emit an event, and then all the components in this hierarchy have to listen to those events and emit all the way up to the original component that is the only one actually allowed to mutate the object?
Is there any better way of doing this?
To not chain events to perform parent/deep child (or do deep ancestor/sibling communication), for simple scenarios, you can use an event hub. (For complex scenarios, see Vuex.)
You would create a global variable:
var eventHub = new Vue(); // use a Vue instance as event hub
To emit events you would use in any component:
eventHub.$emit('myevent', 'some value');
You would, then, in any other component, listen to that event. The action of that event could be anything, including a function that changes the data:
mounted() {
eventHub.$on('myevent', (e) => {
console.log('myevent received', e)
this.someDataProperty = 'newValue' + e;
});
}
Demo:
var eventHub = new Vue(); // use a Vue instance as event hub
Vue.component('some-component', {
template: "#some-component",
props: ['someObject']
});
Vue.component('another-component', {
template: "#another-component",
props: ['someObject'],
methods: {
myMethod() {
eventHub.$emit('myevent', ' <some event value> ' + this.someObject.toLowerCase());
}
}
})
new Vue({
el: '#app',
data: {
something: 'initial value'
},
created() {
eventHub.$on('myevent', (e) => {
this.something = 'newValue-' + e;
});
}
})
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<template id="some-component">
<div>
some-component: {{ someObject }}
<hr>
<another-component :some-object="someObject" />
</div>
</template>
<template id="another-component">
<div>
another-component: {{ someObject }}<br>
<button #click="myMethod">Trigger event at 3rd level that will call some-component's method</button>
</div>
</template>
<div id="app">
Parent: {{ something }}
<hr>
<some-component :some-object="something" />
</div>
Note: If creating a dedicated instance as event hub is something complicated in your environment, you can replace eventHub with this.$root (inside your components) and use your own Vue instance as hub.
In my SPA app, I have an <app-view> wrapper which handles base app code (load user data, render navbar and footer, etc) and has a slot for rendering the actual page. This slot is rendered only if the user data is available.
This wrapper was created because some pages needed a different base code, therefore I couldn't keep this base code in the main app containing <router-view> anymore.
I tried looking if vue-router provides advanced options or suggests a design pattern for switching base code, didn't find anything.
The problem is that the child component will be rendered before the parent component is mounted, i.e. before the parent decides not to render the child component (because it's loading user data). This causes errors like undefined as no attribute foo.
Because of that, I'm looking for a way to defer child rendering until its parent is mounted.
I had a similar problem though not with a SPA. I had child components that needed data from the parent. The problem is that the data would only be generated after the parent has finished mounting so I ended up with null values in the children.
This is how I solved it. I used v-if directive to mount the children only after the parent has finished mounting. (in the mounted() method) see the example below
<template>
<child-component v-if="isMounted"></child-component>
</template>
<script>
data() {
isMounted: false
}, mounted() {
this.isMounted = true
}
</script>
After that, the child could get the data from the parent.
It is slightly unrelated but I hope it gives you an idea.
After trying a few options, it looks like I need to bite the bullet and explicitly define the data that my components depend on, like so:
<app-view>
<div v-if='currentProfile'>
...
</div>
</div>
(currentProfile is received from vuex store getter, and is fetched within app-view)
For any of you that wants to show the child component as soon as the parent components gets data from an API call then you should use something like this:
<template>
<child-component v-if="itemsLoaded"></child-component>
</template>
<script>
data() {
itemsLoaded: false
},
methods: {
getData() {
this.$axios
.get('/path/to/endpoint')
.then((data) => {
// do whatever you need to do with received data
// change the bool value here
this.itemsLoaded = true
})
.catch((err) => {
console.log(err)
})
},
},
mounted() {
this.getData()
// DONT change the bool value here; papa no kiss
this.itemsLoaded = true
}
</script>
If you try to change the boolean value this.itemsLoaded = true in the mounted() method, after calling the getData() method, you will get inconsistent results, since you may or may not receive the data before the this.itemsLoaded = true is executed.
You can actually put the v-if on the <slot> tag in your component.
new Vue({
el: '#app',
render: function(createElement) {
return createElement(
// Your application spec here
{
template: `<slotty :show="showSlot"><span> here</span></slotty>`,
data() {
return {
showSlot: false
}
},
components: {
slotty: {
template: `<div>Hiding slot<slot v-if="show"></slot>.</div>`,
props: ['show']
}
},
mounted() {
setTimeout(() => this.showSlot = true, 1500);
}
}
);
}
})
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
</div>
I have ImageUpload.vue component which is simple Bootstrap modal with few methods.
I'm wondering what is the best way to use one of those methods? Something like this:
const app = new Vue({
components: {
'component-image-upload': require('./components/ImageUpload.vue'),
},
el: '#app',
data() {
return {
images: [],
pagination: {
previous: null,
next: null
}
}
},
methods: {
createImage: function(page) {
this['component-image-upload'].load(image)
}
}
});
The idea is simply to reuse that model to edit existing image data.
When you find yourself calling methods from another (child) component you should rethink your architecture. Vue components are reusable parts you can use to assemble a view, they are not meant to share javascript logic across your application. If you want to do that you're better off writing a module and importing its functions in your parent Vue file. The way you "communicate" with your child components properly is by prop-bindings, which contain data and can re-trigger the render loop of your child component. Communicating from the child to the parent is done by dispatching events. You never want to share logic between the two.
Regardless here's how you can do it anyway:
You have to have an element of your child component in the parent's template, otherwise it wont be mounted and its methods can't be accessed:
<component-image-upload ref="image" />
Then you can call any method on that component by using its ref:
createImage (page) {
this.$refs.image.myMethod('this is how you call it from parent')
}
The best way to do this is Custom Event
You can trigger any event from your component to the #root element which you have loaded in vue instance.
$emit('event-name') can be used to trigger the event!!
I hope this helps :)