26

I have an situation where I have a render function that passes some data to a scoped slot. As part of this data I'd like to include some VNodes constructed by the render function that could optionally be used by the scoped slot. Is there anyway when writing the scoped slot in a template to output the raw VNodes that were received?

Emanon
  • 869
  • 4
  • 11
  • 11
  • Yeah, that's an interesting idea and sounds like maybe the thing to do. Which component in that sample program did that? I see, TodoList, BaseInputText, and TodoListItem. – Emanon Mar 18 '18 at 21:43
  • 2
    Man, I goofed on that; [this is the correct link](https://stackoverflow.com/questions/48975136/how-to-render-a-list-of-static-content-with-vue-named-slot/48975908#48975908). It's the vnode component in the second example. – Bert Mar 18 '18 at 21:46
  • I like it. I suspect this will work for me. Thank you for sharing. – Emanon Mar 18 '18 at 21:49

3 Answers3

57

Vue 3

You can use <component> in the template to render the vnode:

<SomeComponent v-slot="{ vnode }">
  <div>
    <component :is="vnode"/>
  </div>
</SomeComponent>

This only seems to work for a single vnode. If you want to render multiple vnodes (an array) then use v-for or render it as a functional component:

<SomeComponent v-slot="{ vnodes }">
  <div>
    <component :is="() => vnodes"/>
  </div>
</SomeComponent>

or ensure the vnode is a single <Fragment> containing the child vnodes.

You can also use a similar approach to the Vue 2 way below.

Vue 2

You can use a functional component to render the vnodes for that section of your template:

<SomeComponent v-slot="{ vnodes }">
  <div>
    <VNodes :vnodes="vnodes"/>
  </div>
</SomeComponent>
components: {
  VNodes: {
    functional: true,
    render: (h, ctx) => ctx.props.vnodes
  }
}
Decade Moon
  • 32,968
  • 8
  • 81
  • 101
  • **Vue 3 example** https://stackoverflow.com/a/73193775/4563136 – Noel Schenk Aug 01 '22 at 12:36
  • Isn't `` a undocumented way of using component? I thought we are only allowed to pass strings or component definitions, the TS is complaining that `Type VNode is not assignable to type string | ComponentDefinition`. So I'm wondering if it's safe to use it that way or one day it will break. – Kamil Latosinski Apr 25 '23 at 13:39
1

Vue 3 example

<some-component>
   <vnodes :vnodes="vnodes"/>
</some-component>

components: {
  vNodes: {
    props: ['vnodes'],
    render: (h) => h.vnodes,
  },
},
Noel Schenk
  • 724
  • 1
  • 8
  • 19
0

In vue3 with typescript you'd do this:

Define a wrapper functional component.

This is necessary because vue doesn't allow you to mix and match function and template functions.

const VNodes = (props: any) => {
  return props.node;
};

VNodes.props = {
  node: {
    required: true,
  },
};

export default VNodes;

Use the functional component in a template component

<script lang="ts" setup>
import { defineProps } from "vue";
import type { VNode } from "vue";

import VNodes from "./VNodes"; // <-- this is not a magic type, its the component from (1)
const props = defineProps<{ comment: string; render: () => VNode }>();
</script>

<template>
  <div>{{ props.comment }} <v-nodes :node="props.render()" /></div>
</template>

Pass the VNode to the component

import type { Story } from "@storybook/vue3";
import { h } from "vue";
import type { VNode } from "vue";
import ExampleComponent from "./ExampleComponent.vue";

export default {
  title: "ExampleComponent",
  component: ExampleComponent,
  parameters: {},
};

interface StoryProps {
  render: () => VNode;
  comment: string;
}

const Template: Story<StoryProps> = (props) => ({
  components: { ExampleComponent },
  setup() {
    return {
      props,
    };
  },
  template: `<example-component :render="props.render" :comment="props.comment"/>`,
});

export const Default = Template.bind({});
Default.args = {
  comment: "hello",
  render: () => h("div", {}, ["world"]),
};

export const ChildComponent = Template.bind({});
ChildComponent.args = {
  comment: "outer",
  render: () =>
    h(
      ExampleComponent,
      {
        comment: "inner",
        render: () => h("div", {}, ["nested component"]),
      },
      []
    ),
};

Long story short:

The above is a work around by rendering vnodes in a functional component (no template), and rendering the functional component in a template.

So, no. You cannot render VNodes from templates.

This is functionality that vue doesn't have, compared to other component systems.

Doug
  • 32,844
  • 38
  • 166
  • 222