I currently have a component with a render function, to which I send data using a slot.
In short my component looks as follows:
export default {
render(createElement){
return createElement(
'div', {
'class' : 'className'
},
this.$slots.default
)
}
}
There is a bit more inside the render function that creates multiple elements and puts the slot content in each of the element (which is the reason I'm using a render function), but that's not relevant for this example.
I have another component which has this template:
<Component1>
<div>foo</div>
</Component1>
(component 1 being the component with the render function).
This all works nicely, but the problem is that when the word 'foo' changes, the component doesn't get updated. I can send a prop to the component to check wether the content gets changed (by putting a watcher on the prop), but how can I force the component to run the render function again?
Thanks!
Related
Minimal Complete Verifiable Example
This CodeSandbox is the MCVE.
Problem Description
To describe my problem briefly, I have the following structure of renderless components (all use scoped slots and only render the first item of those):
FragmentA that are meant to be used with FragmentBs inside them
FragmentB that are meant to be used with FragmentCs inside them
FragmentC that will render its scoped slot
The problem lies in the fact that on occasion, depending on some input, FragmentC should render a FragmentB instead.
In order to avoid to repeat the FragmentB part in the userland template, I'd like to be able to render FragmentB's scoped slot inside FragmentC using input data from FragmentC.
This sort of "recursion" is ensured to have a base case that renders FragmentC's own slot.
What I've tried so far
I have tried to get access to the slot directly using the provide/inject API, how the scoped itself is accessed might not be the problem, I believe it comes from how I try to render it.
Render as only child
// FragmentC.vue
{
inject: ["$fragB"],
components: { FragmentB },
render(h){
return /*some condition*/
? this.$scopedSlots.default(/* render own scoped slot */)[0]
: h(
FragmentB,
{},
this.$fragB.$scopedSlots.default(/* render FragmentB scoped slot */)[0]
);
}
}
This will result, in the recursively rendered FragmentB, in this.$scopedSlots.default not being a function.
Render as an array of children
// FragmentC.vue
{
inject: ["$fragB"],
components: { FragmentB },
return /*some condition*/
? this.$scopedSlots.default(/* render own scoped slot */)[0]
: [h(
FragmentB,
{},
this.$fragB.$scopedSlots.default(/* render FragmentB scoped slot */)[0]
)];
}
This will cause an infinite recursion.
The concrete question in one sentence
How does one go about programatically rendering another component's scoped slot inside of another component ?
After some digging and a bit of fiddling with the code, I found that the following solution allowed to render properly :
// FragmentC.vue
{
inject: ["$fragB"],
components: { FragmentB },
render(h){
return /*some condition*/
? this.$scopedSlots.default(/* render own scoped slot */)[0]
: h(
FragmentB,
{
props: {/*fragB props*/},
scopedSlots: {
default: this.$fragB.$scopedSlots.default(/* render FragmentB scoped slot */)[0],
}
},
[
this.$fragB.$scopedSlots.default(/* render FragmentB scoped slot */)[0],
]
);
}
}
I have a component which is passed content via a slot. I'm using a render function to output the content. The reason I'm using a render function is because I want to duplicate the content multiple times. When I use this code, everything works fine:
render(createElement){
return createElement('div', {}, this.$slots.default);
}
When I data that is being passed changes, the output changes as well.
However, since I want to duplicate the slot content, I'm now trying this:
return createElement(
'div', {},
[
createElement('div', { }, this.$slots.default),
createElement('div', { }, this.$slots.default)
]
)
Now the problem is, when the slot content changes from outside the component, only the content in the second div gets updated, the content in the first div stays the same..
Am I missing something here?
I can't explain why it happens. But the doc does mention that "VNodes Must Be Unique" in a render function. See https://v2.vuejs.org/v2/guide/render-function.html#Constraints.
Anyway, this is a VNode cloning function, which works, which I discovered from https://jingsam.github.io/2017/03/08/vnode-deep-clone.html.
function deepClone(vnodes, createElement) {
function cloneVNode(vnode) {
const clonedChildren = vnode.children && vnode
.children
.map(vnode => cloneVNode(vnode));
const cloned = createElement(vnode.tag, vnode.data, clonedChildren);
cloned.text = vnode.text;
cloned.isComment = vnode.isComment;
cloned.componentOptions = vnode.componentOptions;
cloned.elm = vnode.elm;
cloned.context = vnode.context;
cloned.ns = vnode.ns;
cloned.isStatic = vnode.isStatic;
cloned.key = vnode.key;
return cloned;
}
const clonedVNodes = vnodes.map(vnode => cloneVNode(vnode))
return clonedVNodes;
}
How to use it:
render(createElement) {
return createElement('div', {}, [
createElement('div', {}, this.$slots.default),
createElement('div', {}, [...deepClone(this.$slots.default, createElement)])
])
}
Demo: https://jsfiddle.net/jacobgoh101/bz3e0o5m/
I found this SO question searching for a way to render the content of a slot multiple times like e.g. for a generic list that can have a template for the content of a list row, which is used for each item.
As of 2020 (in fact earlier) multiple rendering of a slot can be achieved using scoped slots. This is documented here:
https://v2.vuejs.org/v2/guide/components-slots.html#Other-Examples
The documentation says:
Slot props allow us to turn slots into reusable templates that can render different content based on input props
(obviously, if we can use the template to render different content based on props, we can also use it to render the same content)
The example given right there uses a template instead of a render function, but how to use scoped slots in a render function is fortunately also documented:
https://v2.vuejs.org/v2/guide/render-function.html#Slots
I've written a custom render function for a Vue Component, but when I set the "ref" property in the data object that is passed to the createElement function, nothing shows up in the $refs of the root vm (VueComponent)
Vue.component('sm-form-row', {
render: function (createElement) {
// Create the Row Div and append the columns
return createElement('div', {
class: {
'row': true
},
ref: 'some computed value'
});
}
});
What am I missing, the class is being applied correctly but the $refs keep showing empty.
The ref is beign applied and i made a fiddle to see it that it works.
But,if you want to add a reference to sm-form-row component then you have to add the ref attribute in the parent component.For example in parent component:
<sm-form-row ref="formRow" />
And in your parent component you can access it as:
this.$refs.formRow
Also you will be able to access the methods of the child component.For example if the child component has a method called myMethod you can access it in parent component like this:
this.$refs.formRow.myMethod
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"
);
}
});
I'm having this problem that looks a lot like a bug to me and I can't figure out how to solve it.
I created a generic list component and I tell it what child component it should insert in each item and what are the data it should pass to the child component. I'm passing everything as props along with the list (array) itself.
The problem is that I can't mutate the list props. So I try to copy it to model attribute. Otherwise I get this error:
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders.....
And I can't just make it work in any of the lifecycle events. When I save the file and the hot-reloading reloads the page, the list is there, rendered, full of items. When I press F5 to manually reload the page, it is no more. Everything seems to be alright with code though
So in the parent component I'm doing this:
<List ref="link_list"
:list="this.foo.links" //this is array
:child="'LinkFormItem'" //this is the name of the child component
:section_name="'Links'"
:defaults="{content: '', type: 'facebook'}" />
In the List component I get this:
Template
<li class="" v-for="item in datalist">
<component :is="child" :item="item" ></component>
<button v-on:click='remove(index++)' type="button" name="button" class='red button postfix small'>Remove</button>
</li>
Script
<script>
import Child1 from './Child1'
import Child2 from './Child2'
export default {
name: 'search',
props: ['child', 'list', 'defaults','section_name'], //it is received as 'list'
components: {
Child1, Child2
},
data () {
return {
index: 0,
datalist: [] //i'm trying to copy 'list' to 'datalist'
}
},
beforeMount: function () {
// i'm copying it
for(var k in this.list){
this.datalist.push(this.list[k])
}
},
methods: {
//and here I should change it the way I want
add: function () {
this.datalist.push(this.defaults)
},
getList () {
return this.datalist;
},
remove(index){
var datalist = [];
for(var k in this.datalist){
if(k != index) datalist.push(this.datalist[k]);
}
this.datalist = datalist;
}
}
}
</script>
I don't see any problems with my Script. What is going on??
#edit
Ok, some console.log later I found out what the problem seems to be. The HTTP Request is really taking much longer than the mounting of the component to happen. But when it happens, it is not triggering the update in the list component. Nothing is re-rendered and the list is empty.
Workaround
well I realised the problem was related to propagation. I made a few changes in the code to asure the parent component was updating and changing the model value. but the child component (the list component) was not receiving it.
then I gave up trying to understand why and did the following:
1- used the ref in the child component to force an update in the child component with $forceUpdate and then I was assigning the props to the model in the beforeUpdate event. It was causing an error: an re-rendering loop. The update caused a new update and so on. We could just use a flag to stop it.
2- Instead I just called a child method directly:
this.$refs.link_list.updateList(data.links);
I hate this approach because I think it's way too explicit. But it did the job. Then in the child component a new method:
updateList(list){
this.datalist = list;
}
3- The other possibility that passed through my mind was emitting an event. But I didn't try, too complicated
You can simply do like as follows
data () {
return {
index: 0,
datalist: this.list // to copy props to internal component data
}
},
Once you done about you need to apply data manipulation opertions on new this.datalist , not on this.list
If you don't want to mutate the original list array you can do this:
data () {
return {
index: 0,
datalist: Object.assign({}, this.list)
}
}
I think this will help you