Vue functional component throws _c is not defined error - javascript

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...

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.

How to foward $refs in Vue

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

How can I add a Vue component within some HTML to another Vue component?

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

Use <component> that was imported in the parent

I'm building a component that manages other components.
It dynamically render components in specific places depending on the props and inputs, much like an orchestrator.
Use case
My orchestrator have the following placeholders, like a grid (p1 ... p6):
|-p1-|-p2-|-p3-|
|-p4-|-p5-|-p6-|
In a given moment, it renders the component C1 into p2 and C2 into p6:
|-p1-|-C1-|-p3-|
|-p4-|-p5-|-C2-|
In another moment, it replaces the C1 by C3:
|-p1-|-C3-|-p3-|
|-p4-|-p5-|-C2-|
The problem
Given this dynamism, I can't (as far as I know) use slots. So I'm using component tags with the :is prop in order to dynamically render the correct component. The problem is that in order for the :is to work, I must have the component defined in my orchestrator/manager component. And I would like to keep it separated for reuse, doesn't make sense to import the components there. One solution would be to globally register the components. I would like to avoid that if possible. Is there a way? I'm open to any kind of reflection-magic you may think n_n'
You can just pass the component via a prop like this:
Orchestrator.vue
<component :is="comp"/>
export default {
props: ['comp']
}
Test.vue
<orchestrator :comp="MyComponent"/>
import Orchestrator from './orchestrator.vue'
import MyComponent from './my-component.vue'
export default {
components: {
Orchestrator,
},
data() {
return {
MyComponent,
};
},
}
You should be able to use async components, loaded dynamically.
For example...
<component :is="p2Replacement"></component>
data () {
return {
p2Replacement: null
}
},
methods: {
setP2Replacement(component) {
this.p2Replacement = () => import(`path/to/${component}.vue`)
}
}
And call it like setP2Replacement('C1') or setP2Replacement('C3').
The fixed parts in the import expression are required so Webpack can include the appropriate files using a glob pattern. See https://webpack.js.org/guides/dependency-management/#require-context

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