How to foward $refs in Vue - javascript

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

Related

vue 3 access props passed from parent component that are not defined in child component

hope everything is okay.
i am used to react.js but when i try vue things were a bit different
in react it's very simple accessing props passed from parent in the child component
but in vue i must define each prop to use it.
so i was just wondering if it's possible to pass any prop and use it in the child component without defining it
in react i can do that
// parent component
const Parent = () => {
return (
<Child anyprop="propvalue" />
)
}
const Child = (props) => {
return (
<p>{JSON.stringify(props)}</p>
)
}
and that would work
in vue i can only use props if i define it like this
//parent
<template>
<Child anyprop="value" anotherprop="value"></Child>
</template>
<script setup>
const props = defineProps({
anyprop: String
})
//child
<template>
<p>{{props}}</p>
</template>
<script setup>
const props = defineProps({
anyprop: String
})
</script>
if i do that in vue i can only see "anyprop" not the "anotherprop" i must define it in the define props block to use it
any ideas what can i do to achieve something like what react.js offers
All data that isn't defined in props goes to attributes. Despite the name, they aren't HTML attributes and can have non-string values.
In a template they are available as $attrs:
<Child :anyprop="$attrs.anyprop" anotherprop="value"/>
A more common case is to pass all attributes instead of enumerating them, this is a counterpart to React {...rest}:
<Child v-bind="$attrs" anotherprop="value"/>
This may be not needed for root element like Child because attribute fallthrough is done by default, otherwise inheritAttrs: false should be used.
This requires Child to have anyprop prop declared.
In a script, attributes are available as context.attrs in setup function and useAttrs in script setup, they can be used to compute props for nested elements.

VueJS mounted hook in v-on directive

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.

Cannot pass parent component's object as prop in Vue after Laravel Mix update

I recently updated laravel mix in my project to the latest and immediately I was swarmed by Vue errors. I seem to struggle with this one especially. I have a component:
<template>
<div>
<ChildComponent :context="this"></ChildComponent>
</div>
</template>
<script>
import ChildComponent from './child-component';
export default {
components: { ChildComponent },
...
}
</script>
This is the error I get: Error in render: "TypeError: Cannot read property 'context' of undefined".
Seems weird because in vue-devtools the parent component is present as an object in context prop of ChildComponent.
I should probably also add that context is defined as context: {} in ChildComponent's props.
this is not defined in the <template> and attempting to pass the entire Vue instance to the child seems like an anti-pattern. You should explicitly define and pass data and variables to children components, and (optionally) if you want to receive some data back from the child, you should emit events and bind to those events in the parent.
If you must access the parent component from the child, you can use this.$parent in the child component: https://v2.vuejs.org/v2/guide/components-edge-cases.html#Accessing-the-Parent-Component-Instance
But unless you have a good reason to reach for that $parent variable, it should be avoided as it tightly couples your components together.

Vue functional component throws _c is not defined error

I created the following functional component using render method:
import Vue from "vue"
const { render, staticRenderFns } = Vue.compile(`<div>Hello World</div>`)
Vue.component("HelloWorld", {
functional: true,
render,
staticRenderFns
})
And then in App.vue:
<template>
<div id="app">
<HelloWorld />
</div>
</template>
<script>
export default {
data() {
return {
compiled: false
}
}
}
</script>
<style>
</style>
And I get the error:
_c is not defined.
Am I doing something wrong here?
As far as I'm aware, the render functions generated by Vue.compile cannot be used to render a functional component.
I think the closest thing to creating a functional component from a template string would involve parsing the string to get the element type and attributes and render as:
Vue.component("hello-world", {
functional: true,
render: function(createElement, context) {
return createElement(
"div",
'example text'
);
}
});
As Decade Moon already mentioned, render functions returned by Vue.compile cannot be used as render function in functional component. Reason for that becomes clear when you inspect the signature of the function returned by Vue.compile:
const render: (createElement: any) => VNode
As you can see, function is missing second argument, which is required for render functions of functional components:
render: (createElement: CreateElement, context: RenderContext<Record<never, any>>) => VNode
Functional components are instanceless. That means no this context - that's why additional context parameter is needed to carry props\listeners etc.
Also if you look at this post on Vue forum:
The render function created by compile() relies on private properties of the component. To have access to those properties, the method has to be assigned to a property of the component(so it has access to those properties via this)
However that doesn't mean you can't create functional component with template. You can, you are just not able to use Vue.compile to pass template text dynamically. If you are fine with static template, you can do it like this:
// component.vue
<template functional>
<div>Hello World</div>
</template>
<script>
export default {
name: "hello"
};
</script>
...and use the component like any other SFC (single file component = VUE file)
If you need dynamic template text, just use non-functional component instead...

How to apply classes to Vue.js Functional Component from parent component?

Suppose I have a functional component:
<template functional>
<div>Some functional component</div>
</template>
Now I render this component in some parent with classes:
<parent>
<some-child class="new-class"></some-child>
</parent>
Resultant DOM doesn't have new-class applied to the Functional child component. Now as I understand, Vue-loader compiles Functional component against render function context as explained here. That means classes won't be directly applied and merge intelligently.
Question is - how can I make Functional component play nicely with the externally applied class when using a template?
Note: I know it is easily possible to do so via render function:
Vue.component("functional-comp", {
functional: true,
render(h, context) {
return h("div", context.data, "Some functional component");
}
});
TL;DR;
Use data.staticClass to get the class, and bind the other attributes using data.attrs
<template functional>
<div class="my-class" :class="data.staticClass || ''" v-bind="data.attrs">
//...
</div>
</template>
Explanation:
v-bind binds all the other stuff, and you may not need it, but it will bind attributes like id or style. The problem is that you can't use it for class because that's a reserved js object so vue uses staticClass, so binding has to be done manually using :class="data.staticClass".
This will fail if the staticClass property is not defined, by the parent, so you should use :class="data.staticClass || ''"
Example:
I can't show this as a fiddle, since only "Functional components defined as a Single-File Component in a *.vue file also receives proper template compilation"
I've got a working codesandbox though: https://codesandbox.io/s/z64lm33vol
Render function inside a functional component example, to supplement #Daniel's answer:
render(createElement, { data } {
return createElement(
'div',
{
class: {
...(data.staticClass && {
[data.staticClass]: true,
})
},
attrs: {
...data.attrs,
}
}
)
}
We can use ES6 computed property name in order to set a static class.
You have to use props to pass attributes to components
https://v2.vuejs.org/v2/guide/components.html#Passing-Data-to-Child-Components-with-Props

Categories

Resources