How to render another component's scoped slot? - vue.js

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],
]
);
}
}

Related

VueJS: How to to access parent function in child to conditionally render css classes?

I have a parent component with a function like (simplified example)
isValid(value) { return value > validationModifier }
Both the parent and the child use that function to conditionally render e.g. CSS classes. So in my child I would like to use:
:class="{'my-class' : isValid(myValue)}"
But I don't have access to this function. I want to avoid duplicating it in the child, and I don't see how emitting an event would work in this case.
What is the appropriate way to deal with this?
If the function has reusable logic, rather than specific to that parent component, then I would use a mixin. If you want to add any other shared logic (methods, computed functions) you can edit the mixin and don't have to explicitly add the new parameter to parent and child
mixin code:
const myMixin = {
methods:{
isValid(param1){
return param1 < validationModifier
}
}
}
then to inject into any of your components
{
name: "my-custom-component",
mixins:[myMixin],
methods:{}
}
You can pass the function to the child like a classical function prop https://v2.vuejs.org/v2/guide/components-props.html#Prop-Types
No need to use the event/emit system here.
<child v-bind:is-valid="isValid"></child>
#Joel H's answer is one of the ways to reuse functions in Vue. Another way is to use dependency injection in Vue. See https://v2.vuejs.org/v2/guide/components-edge-cases.html#Dependency-Injection
You just have to provide the method and all the children components of the ParentComponent can access that isValid method. Dependency injection in Vue is not limited to functions only, you can pass variables and data too.
export default {
name: 'ParentComponent',
...
methods: {
isValid(value) { return value > validationModifier },
},
provide() {
return {
isValid: this.isValid
}
}
}
and in your ChildComponent ...
export default {
name: 'ChildComponent',
...
inject: ['isValid']
}
Now you can use the function in your ChildComponent using this.isValid(yourValueHere).

How can I duplicate slots within a Vuejs render function?

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

Passing data to Vue.js component

I am creating a component and want to pass two properties (item & brokerageID) to the component. Here is the HTML code:
{{brokerageID}}
<holiday-component v-bind:item="item" v-bind:brokerageID="brokerageID" testID="45" ></holiday-component>
Here is the code for 'holiday-component'
Vue.component('holiday-component', {
props: ['item',
'brokerageID',
'testID',
],
data () {
return {
holidaysData: [],
showHolidays: false,
}
},
methods: {
getHolidays(contactID) {
....
},
template: <div> {{testID}} {{item.contactName}} {{brokerageID}}
....
The 'item' property is getting passed to the component (item.contactName is displayed correctly in the component template. However, somehow, brokerageID (property of the Vue object) is not getting passed. This property exists which is confirmed as {{brokerageID}} used above the component in HTML displays value. But, within the component template, brokerageID is not available. Also, the testID property passed to the component is not displayed.
Could someone please advise, what is wrong in my implementation that I am unable to use brokerageID in my component?
See Vue's docs about prop naming https://v2.vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
In this instance, using v-bind:brokerage-id and v-bind:test-id should do the trick.

Vuejs: How to trigger render function?

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!

VueJS access child component's data from parent

I'm using the vue-cli scaffold for webpack
My Vue component structure/heirarchy currently looks like the following:
App
PDF Template
Background
Dynamic Template Image
Static Template Image
Markdown
At the app level, I want a vuejs component method that can aggregate all of the child component's data into a single JSON object that can be sent off to the server.
Is there a way to access child component's data? Specifically, multiple layers deep?
If not, what is the best practice for passing down oberservable data/parameters, so that when it's modified by child components I have access to the new values? I'm trying to avoid hard dependencies between components, so as of right now, the only thing passed using component attributes are initialization values.
UPDATE:
Solid answers. Resources I found helpful after reviewing both answers:
Vuex and when to use it
Vuex alternative solution for smaller apps
In my child component, there are no buttons to emit changed data. It's a form with somewhat 5~10 inputs. the data will be submitted once you click the process button in another component. so, I can't emit every property when it's changing.
So, what I did,
In my parent component, I can access child's data from "ref"
e.g
<markdown ref="markdowndetails"></markdown>
<app-button #submit="process"></app-button>
// js
methods:{
process: function(){
// items is defined object inside data()
var markdowns = this.$refs.markdowndetails.items
}
}
Note: If you do this all over the application I suggest move to vuex instead.
For this kind of structure It's good to have some kind of Store.
VueJS provide solution for that, and It's called Vuex.If you are not ready to go with Vuex, you can create your own simple store.
Let's try with this
MarkdownStore.js
export default {
data: {
items: []
},
// Methods that you need, for e.g fetching data from server etc.
fetchData() {
// fetch logic
}
}
And now you can use those data everywhere, with importing this Store file
HomeView.vue
import MarkdownStore from '../stores/MarkdownStore'
export default {
data() {
sharedItems: MarkdownStore.data
},
created() {
MarkdownStore.fetchData()
}
}
So that's the basic flow that you could use, If you dont' want to go with Vuex.
what is the best practice for passing down oberservable data/parameters, so that when it's modified by child components I have access to the new values?
The flow of props is one way down, a child should never modify its props directly.
For a complex application, vuex is the solution, but for a simple case vuex is an overkill. Just like what #Belmin said, you can even use a plain JavaScript object for that, thanks to the reactivity system.
Another solution is using events. Vue has already implemented the EventEmitter interface, a child can use this.$emit('eventName', data) to communicate with its parent.
The parent will listen on the event like this: (#update is the shorthand of v-on:update)
<child :value="value" #update="onChildUpdate" />
and update the data in the event handler:
methods: {
onChildUpdate (newValue) {
this.value = newValue
}
}
Here is a simple example of custom events in Vue:
http://codepen.io/CodinCat/pen/ZBELjm?editors=1010
This is just parent-child communication, if a component needs to talk to its siblings, then you will need a global event bus, in Vue.js, you can just use an empty Vue instance:
const bus = new Vue()
// In component A
bus.$on('somethingUpdated', data => { ... })
// In component B
bus.$emit('somethingUpdated', newData)
you can meke ref to child component and use it as this
this.$refs.refComponentName.$data
parent-component
<template>
<section>
<childComponent ref="nameOfRef" />
</section>
</template>
methods: {
save() {
let Data = this.$refs.nameOfRef.$data;
}
},
In my case I have a registration form that I've broken down into components.
As suggested above I used $refs, In my parent I have for example:
In Template:
<Personal ref="personal" />
Script - Parent Component
export default {
components: {
Personal,
Employment
},
data() {
return {
personal: null,
education: null
}
},
mounted: function(){
this.personal = this.$refs.personal.model
this.education = this.$refs.education.model
}
}
This works well as the data is reactive.