Vue render function: include slots to child component without wrapper - vue.js

I'm trying to make a functional component that renders a component or another depending on a prop.
One of the output has to be a <v-select> component, and I want to pass it down all its slots / props, like if we called it directly.
<custom-component :loading="loading">
<template #loading>
<span>Loading...</span>
</template>
</custom-component>
<!-- Should renders like this (sometimes) -->
<v-select :loading="loading">
<template #loading>
<span>Loading...</span>
</template>
</v-select>
But I can't find a way to include the slots given to my functional component to the I'm rendering without adding a wrapper around them:
render (h: CreateElement, context: RenderContext) {
// Removed some logic here for clarity
return h(
'v-select',
{
props: context.props,
attrs: context.data.attrs,
on: context.listeners,
},
[
// I use the `slot` option to tell in which slot I want to render this.
// But it forces me to add a div wrapper...
h('div', { slot: 'loading' }, context.slots()['loading'])
],
)
}
I can't use the scopedSlots option since this slot (for example) has no slot props, so the function is never called.
return h(
'v-select',
{
props: context.props,
attrs: context.data.attrs,
on: context.listeners,
scopedSlots: {
loading(props) {
// Never called because no props it passed to that slot
return context.slots()['loading']
}
}
},
Is there any way to pass down the slots to the component i'm rendering without adding them a wrapper element?

I found out it's totally valid to use the createElement function to render a <template> tag, the same used to determine which slot we are on.
So using it like this fixes my problem:
render (h: CreateElement, context: RenderContext) {
// Removed some logic here for clarity
return h(
'v-select',
{
props: context.props,
attrs: context.data.attrs,
on: context.listeners,
},
[
// I use the `slot` option to tell in which slot I want to render this.
// <template> vue pseudo element that won't be actually rendered in the end.
h('template', { slot: 'loading' }, context.slots()['loading'])
],
)
}

In Vue 3 it's a way easier.
Check the docs Renderless Components (or playground)
An example from the docs:
App.vue
<script setup>
import MouseTracker from './MouseTracker.vue'
</script>
<template>
<MouseTracker v-slot="{ x, y }">
Mouse is at: {{ x }}, {{ y }}
</MouseTracker>
</template>
MouseTracker.vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
<template>
<slot :x="x" :y="y"/>
</template>
Just to mention, you can easily override CSS of the component in the slot as well.

Related

How to get slot props with render functions in Vue?

I'm trying to transform a Vue component I've made in a render function. Problem is I can't find a way to access the named slot props, like in the example:
<template #slot-name="slotProps">
<MyComponent v-bind="slotProps" />
</template>
There's a way to transform this code in a render function ?
To pass a scoped slot, use the scopedSlots property of the 2nd argument to h() (createElement()) in the form of { name: props => VNode | Array<VNode> }.
For example, assuming your template is:
<MySlotConsumer>
<template #mySlot="slotProps">
<MyComponent v-bind="slotProps" />
</template>
</MySlotConsumer>
The equivalent render function would be:
export default {
render(h) {
return h(MySlotConsumer, {
scopedSlots: {
mySlot: slotProps => h(MyComponent, { props: slotProps })
}
})
}
}
demo

create a reusable component in vue.js

How do I make my vue.js component more general and reusable and can pass only the neccessary data?
this is what I want to build:
Structure:
the component has a header
the component has different inputs
the component has a submit button
the component has a list as a footer - that is passed depending on the input
My Approach
the parent
// App.vue
<template>
<div>
<!-- Component A -->
<SettingsCard
:listA="listA"
cardType="CompA"
>
<template v-slot:header>
Foo - Component A
</template>
</SettingsCard>
<!-- Component B -->
<SettingsCard
:listB="listB"
cardType="CompB"
>
<template v-slot:header>
Bar - Component B
</template>
</SettingsCard>
</div>
</template>
the child:
// SettingsCard.vue
<template>
<div>
<slot name="header"></slot>
<div v-if="cardType === 'CompA'">
<!-- Show input and submit button for component a -->
</div>
<div v-if="cardType === 'CompB'">
<!-- Show input and submit button for component b -->
</div>
<ListComponent
:cardType="cardType"
:list="computedList"
/>
</div>
</template>
<script>
export default {
props: {
cardType: String, // for the v-if conditions
listA: Array,
ListB: Array
},
data() {
return {
namefromCompA: '', // input from component A
namefromCompB: '' // input from component B
}
},
computed: {
computedList() {
// returns an array and pass as prop the the card footer
}
}
}
</script>
The problems
I have undefined props and unused data in my SettingsCard.vue component
// CompA:
props: {
cardType: 'compA',
listA: [1, 2, 3], // comes from the parent
listB: undefined // how to prevent the undefined?
}
// CompA:
data() {
return {
namefromCompA: 'hello world',
namefromCompB: '' // unused - please remove me
}
}
to use v-if="cardType === 'compA'" feels wrong
Do you have a better approach in mind to make this component reusable and remove anything unnecessary?
use a method instead of "cardType === 'CompA'".
just try this in your SettingsCard.vue
methods: {
showMeWhen(type) {
return this.cardType === type;
},
},
}
and your v-if render condition would be like:
v-if="showMeWhen('compA')"
update
for exmaple in your namefromCompA/B you can just pass a new prop to display the correct name.
props: {
cardType: String, // for the v-if conditions
listA: Array,
ListB: Array,
namefromComponent: {
type: String,
default: 'NoName'
}
},
then in your usage you just pass it like you do with the other props.
<SettingsCard
:listB="listB"
cardType="CompB"
namefrom-component="my Name for component B"
>

Vue.js Generic Dynamic Component Rendering

I have a component that dynamic render components that get as props
<template>
<div>
<component :is="component" :data="data" v-if="component" />
</div>
</template>
<script>
export default {
name: 'dynamic-component-renderer',
props: ['data', 'type'],
computed: {
component() {
if (!this.type) {
return null;
}
return this.type;
},
}
}
</script>
The issue is in imports, I need to dynamic import, I know that I can do dynamic importing with webpack like this: () => import('./my-async-component'), but in my case, I don't need lazyLoad.
So I need to have a generic dummy component (dynamic-component-renderer) that will not know what components will get and dynamically render.

Vue component wrapping

What is the correct way to wrap a component with another component while maintaining all the functionality of the child component.
my need is to wrap my component with a container, keeping all the functionality of the child and adding a trigger when clicking on the container outside the child that would trigger the child`s onclick event,
The parent component should emit all the child component events and accept all the props the child component accepts and pass them along, all the parent does is add a clickable wrapper.
in a way im asking how to extend a component in vue...
It is called a transparent wrapper.
That's how it is usually done:
<template>
<div class="custom-textarea">
<!-- Wrapped component: -->
<textarea
:value="value"
v-on="listeners"
:rows="rows"
v-bind="attrs"
>
</textarea>
</div>
</template>
<script>
export default {
props: ['value'], # any props you want
inheritAttrs: false,
computed: {
listeners() {
# input event has a special treatment in this example:
const { input, ...listeners } = this.$listeners;
return listeners;
},
rows() {
return this.$attrs.rows || 3;
},
attrs() {
# :rows property has a special treatment in this example:
const { rows, ...attrs } = this.$attrs;
return attrs;
},
},
methods: {
input(event) {
# You can handle any events here, not just input:
this.$emit('input', event.target.value);
},
}
}
</script>
Sources:
https://www.vuemastery.com/conferences/vueconf-us-2018/7-secret-patterns-vue-consultants-dont-want-you-to-know-chris-fritz/
https://zendev.com/2018/05/31/transparent-wrapper-components-in-vue.html

Reusable component to render button or router-link in Vue.js

I'm new using Vue.js and I had a difficulty creating a Button component.
How can I program this component to conditional rendering? In other words, maybe it should be rendering as a router-link maybe as a button? Like that:
<Button type="button" #click="alert('hi!')">It's a button.</Button>
// -> Should return as a <button>.
<Button :to="{ name: 'SomeRoute' }">It's a link.</Button>
// -> Should return as a <router-link>.
You can toggle the tag inside render() or just use <component>.
According to the official specification for Dynamic Components:
You can use the same mount point and dynamically switch between multiple components using the reserved <component> element and dynamically bind to it's is attribute.
Here's an example for your case:
ButtonControl.vue
<template>
<component :is="type" :to="to">
{{ value }}
</component>
</template>
<script>
export default {
computed: {
type () {
if (this.to) {
return 'router-link'
}
return 'button'
}
},
props: {
to: {
required: false
},
value: {
type: String
}
}
}
</script>
Now you can easily use it for a button:
<button-control value="Something"></button-control>
Or a router-link:
<button-control to="/" value="Something"></button-control>
This is an excellent behavior to keep in mind when it's necessary to create elements that may have links or not, such as buttons or cards.
You can create a custom component which can dynamically render as a different tag using the v-if, v-else-if and v-else directives. As long as Vue can tell that the custom component will have a single root element after it has been rendered, it won't complain.
But first off, you shouldn't name a custom component using the name of "built-in or reserved HTML elements", as the Vue warning you'll get will tell you.
It doesn't make sense to me why you want a single component to conditionally render as a <button> or a <router-link> (which itself renders to an <a> element by default). But if you really want to do that, here's an example:
Vue.use(VueRouter);
const router = new VueRouter({
routes: [ { path: '/' } ]
})
Vue.component('linkOrButton', {
template: `
<router-link v-if="type === 'link'" :to="to">I'm a router-link</router-link>
<button v-else-if="type ==='button'">I'm a button</button>
<div v-else>I'm a just a div</div>
`,
props: ['type', 'to']
})
new Vue({ el: '#app', router })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
<link-or-button type="link" to="/"></link-or-button>
<link-or-button type="button"></link-or-button>
<link-or-button></link-or-button>
</div>
If you're just trying to render a <router-link> as a <button> instead of an <a>, then you can specify that via the tag prop on the <router-link> itself:
Vue.use(VueRouter);
const router = new VueRouter({
routes: [ { path: '/' } ]
})
new Vue({ el: '#app', router })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
<router-link to="/">I'm an a</router-link>
<router-link to="/" tag="button">I'm a button</router-link>
</div>
You can achieve that through render functions.
render: function (h) {
if(this.to){ // i am not sure if presence of to props is your condition
return h(routerLink, { props: { to: this.to } },this.$slots.default)
}
return h('a', this.$slots.default)
}
That should help you start into the right direction
I don't think you'd be able to render a <router-link> or <button> conditionally without having a parent element.
What you can do is decide what to do on click as well as style your element based on the props passed.
template: `<a :class="{btn: !isLink, link: isLink}" #click="handleClick"><slot>Default content</slot></a>`,
props: ['to'],
computed: {
isLink () { return !!this.to }
},
methods: {
handleClick () {
if (this.isLink) {
this.$router.push(this.to)
}
this.$emit('click') // edited this to always emit
}
}
I would follow the advice by #Phil and use v-if but if you'd rather use one component, you can programmatically navigate in your click method.
Your code can look something like this:
<template>
<Button type="button" #click="handleLink">It's a button.</Button>
</template>
<script>
export default {
name: 'my-button',
props: {
routerLink: {
type: Boolean,
default: false
}
},
methods: {
handleLink () {
if (this.routerLink) {
this.$router.push({ name: 'SomeRoute' })
} else {
alert("hi!")
}
}
}
}
</script>