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?
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
}
}
Vue 3 example
<some-component>
<vnodes :vnodes="vnodes"/>
</some-component>
components: {
vNodes: {
props: ['vnodes'],
render: (h) => h.vnodes,
},
},
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.
Related
Let's take a step back and look at the use case:
You're defining a modular interface, and any module that implements it must be able to 'render itself' into the application given a slot and a state.
How do you do it in vue?
Example solution
Let's have a look at the most basic implementation I can assemble:
(full example:
https://stackblitz.com/edit/vitejs-vite-8zclnp?file=src/App.vue)
We have a layout:
# Layout.vue
<template>
<div>
<hr />
<slot name="moduleView" />
<hr />
</div>
</template>
...and an app with a module:
# App.vue
<script lang="ts" setup>
import type { MyModuleState } from "./MyModule";
import Layout from "./Layout.vue";
import { ref } from "vue";
import { MyModule } from "./MyModule";
import ModView from "./ModView.vue";
const state = ref<MyModuleState>({ value: 0 });
const module = new MyModule();
const onClick = () => {
state.value = { value: state.value.value + 1 };
};
const renderModule = () => {
return module.view(state.value);
};
</script>
<template>
<div>currentValue: {{ state.value }}</div>
<div>update: <button #click="onClick">++</button></div>
<div>
<Layout>
<template v-slot:moduleView>
<mod-view :render="renderModule" :state="state" /> // <--- But this!
</template>
</Layout>
</div>
</template>
...but rendering into the slot requires a lot of jumping through obscure hoops:
# ModView.vue
<script lang="ts" setup>
import ModRender from "./ModRender";
import { ref, watch } from "vue";
import type { VNode } from "vue";
const props = defineProps<{
render: (state?: any) => VNode | Array<VNode>;
state?: any;
}>();
const nodes = ref(props.render(props.state));
watch( // <-- Obscure! The view won't update unless you explicitly watch props?
() => props.state,
(nextState) => {
nodes.value = props.render(nextState);
}
);
</script>
<template>
<mod-render :nodes="nodes" />
</template>
# ModRender.ts
import type { VNode } from "vue";
const ModRender = (props: { nodes: VNode | Array<VNode> }) => {
return props.nodes;
};
ModRender.props = {
nodes: {
required: true,
},
};
export default ModRender; // <--- Super obscure, why do you need a functional component for this?
Before we can define the actual module:
# MyModule.ts
import type { VNode } from "vue";
import { h } from "vue";
import ModuleView from "./MyModuleDisplay.vue";
interface AbstractModule<T> {
view: (state: T) => VNode;
}
export interface MyModuleState {
value: number;
}
export class MyModule implements AbstractModule<MyModuleState> {
view(state: MyModuleState): VNode {
return h(ModuleView, { state });
}
}
...and a component for it:
# MyModuleView.vue
<script setup lang="ts">
import type { MyModuleState } from "./MyModule";
const props = defineProps<{ state: MyModuleState }>();
</script>
<template>
<div>{{ state.value }}</div>
</template>
What.
This seems extremely obtuse and verbose.
Am I missing something?
In other component systems an implementation might look like:
export class MyModule implements AbstractModule<MyModuleState> {
view(state: MyModuleState): VNode {
return (<div>{state.value}</div>);
}
}
...
<div>
<Layout>{renderModule(state)}</Layout>
</div>
It seems surprising that so many wrappers and hoops have to be done in vue to do this, which makes me feel like I'm missing something.
Is there an easier way of doing this?
Vnode objects cannot be rendered in component templates as is and need to be wrapped in a component like ModRender. If they are used as universal way to exchange template data in the app, that's a problem. Vnodes still can be directly used in component render functions and functional components with JSX or h like <Layout>{renderModule(state)}</Layout>, this limits their usage.
AbstractModule convention may need to be reconsidered if it results in unnecessary code. Proceed from the fact that a "view" needs to be used with dynamic <component> at some point, and it will be as straightforward as possible.
There may be no necessity for "module" abstraction, but even if there is, module.view can return a component (functional or stateful) instead of vnodes. Or it can construct a component and make it available as a property, e.g.:
class MyModule {
constructor(state) {
this.viewComponent = (props) => h(ModuleView, { state, ...props })
}
}
So I have a problem with my vue3 project. The gist: I need to support different layouts for some use cases: authorization, user profile's layout, group's layout, etc. I've got the opportunity by this way:
Create a component AppLayout.vue for managing layouts
<template>
<component :is="layout">
<slot />
</component>
</template>
<script>
import AppLayoutDefault from "#/layouts/EmptyLayout";
import { shallowRef, watch } from "vue";
import { useRoute } from "vue-router";
export default {
name: "AppLayout",
setup() {
const layout = shallowRef(AppLayoutDefault);
const route = useRoute();
watch(
() => route.meta,
async (meta) => {
try {
const component =
await require(`#/layouts/${meta.layout}.vue`);
layout.value = component?.default || AppLayoutDefault;
} catch (e) {
layout.value = AppLayoutDefault;
}
}
);
return { layout };
},
};
</script>
So my App.vue started to look so
<template>
<AppLayout>
<router-view />
</AppLayout>
</template>
To render a specific layout, I've added to router's index.js special tag meta
{
path: '/login',
name: 'LoginPage',
component: () => import('../views/auth/LoginPage.vue')
},
{
path: '/me',
name: 'MePage',
component: () => import('../views/user/MePage.vue'),
meta: {
layout: 'ProfileLayout'
},
},
Now I can create some layouts. For example, I've made ProfileLayout.vue with some nested components: Header and Footer. I use slots to render dynamic page content.
<template>
<div>
<div class="container">
<Header />
<slot />
<Footer />
</div>
</div>
</template>
So, when I type the URL http://example.com/profile, I see the content of Profile page based on ProfileLayout. And here the problem is: Profile page invokes hooks twice.
I put console.log() into created() hook and I see the following
That's problem because I have some requests inside of hooks, and they execute twice too. I'm a newbie in vuejs and I don't understand deeply how vue renders components. I suggest that someting inside of the code invokes re-rendering and Profile Page creates again. How to prevent it?
Your profile page loaded twice because it's literally... have to load twice.
This is the render flow, not accurate but for you to get the idea:
Your layout.value=AppDefaultLayout. The dynamic component <component :is="layout"> will render it first since meta.layout is undefined on initial. ProfilePage was also rendered at this point.
meta.layout now had value & watcher made the change to layout.value => <component :is="layout"> re-render 2nd times, also for ProfilePage
So to resolve this problem I simply remove the default value, the dynamic component is no longer need to render default layout. If it has no value so it should not render anything.
<keep-alive>
<component :is="layout">
<slot />
</component>
</keep-alive>
import { markRaw, shallowRef, watch } from "vue";
import { useRoute } from "vue-router";
export default {
name: "AppLayout",
setup() {
console.debug("Loaded DynamicLayout");
const layout = shallowRef()
const route = useRoute()
const getLayout = async (lyt) => {
const c = await import(`#/components/${lyt}.vue`);
return c.default;
};
watch(
() => route.meta,
async (meta) => {
console.log('...', meta.layout);
try {
layout.value = markRaw(await getLayout(meta.layout));
} catch (e) {
console.warn('%c Use AppLayoutDefault instead.\n', 'color: darkturquoise', e);
layout.value = markRaw(await getLayout('EmptyLayout'));
}
}
);
return { layout }
},
};
I want to call the dialog like this:
import demo from './demo.vue';
methods: {
open() {
const dialog = this.$dialog({
content: demo
});
}
}
dialog.js
import Vue from 'vue';
import QfDialog from './qf-dialog';
import ElementQfUI from 'element-qf-ui';
Vue.use(ElementQfUI);
let DialogConstructor = Vue.extend(QfDialog);
export const dialog = (params) => {
const instance = new DialogConstructor({
propsData: {
visible: true,
...params
}
});
instance.$mount();
document.body.appendChild(instance.$el);
return instance;
}
Vue.prototype.$dialog = dialog;
I tried to generate a VNode from a vue object to use in the template, but it gives me following error:
Error in render: "TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
--- property '_renderProxy' closes the circle"
<template>
<el-dialog :visible.sync="visible" v-bind="$attrs" v-on="$listeners">
{{ contentTpl }}
</el-dialog>
</template>
<script>
export default {
name: 'qf-dialog',
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
contentTpl: null
};
},
created() {
// this.content is a vue obj
let content = JSON.parse(JSON.stringify(this.content));
let vnode = this.$createElement('demo-cc', content);
this.contentTpl = [vnode];
}
};
</script>
How do I make {{contentTpl}} work ?
You are passing a whole Vue component into your Dialog - not a VNodes
Just use Dynamic Components
<template>
<el-dialog :visible.sync="visible" v-bind="$attrs" v-on="$listeners">
<component :is="content" />
</el-dialog>
</template>
Note that using :visible.sync is problematic as Vue does not allow to modify props the component receives
I am completely new to VueJS and NuxtJS. I can't seem to pass a property in the layout to a page component.
This is my layouts/default.vue
<template>
<Nuxt myprop="hello world" />
</template>
<script>
export default {
data: () => ({
myprop: 'hello galaxy',
}),
}
</script>
This is my pages/index.vue
<template>
<div>My Prop is: {{myprop}}</div>
</template>
<script>
export default {
props: {
myprop: {
type: String
},
},
}
</script>
When I load up my app, I expect to see My Prop is: hello world. But instead, I see My Prop is:, and it seems myprop is empty.
What am I doing wrong? How does a layout component pass a property to child component in VueJS or NuxtJS?
You cannot pass props in that way, but you could use provide/inject pattern to pass the data from layout to the page :
layout/default.vue
<template>
<Nuxt />
</template>
<script>
export default {
provide: function () {
return { myprop: this.myprop };
},
data: () => ({
myprop: 'hello galaxy',
}),
}
</script>
pages/index.vue
<template>
<div>My Prop is: {{myprop}}</div>
</template>
<script>
export default {
inject: ["myprop"],
}
</script>
//store
export default {
state: {
aboutModels: []
},
actions: {
findBy: ({commit}, about)=> {
//do getModels
var aboutModels = [{name: 'About'}] //Vue.resource('/abouts').get(about)
commit('setModels', aboutModels)
}
},
getters: {
getModels(state){
return state.aboutModels
}
},
mutations: {
setModels: (state, aboutModels)=> {
state.aboutModels = aboutModels
}
}
}
//component
import {mapActions, mapGetters} from "vuex";
export default {
name: 'About',
template: require('./about.template'),
style: require('./about.style'),
created () {
document.title = 'About'
this.findBy()
},
computed: mapGetters({
abouts: 'getModels'
}),
methods: mapActions({
findBy: 'findBy'
})
}
//view
<div class="about" v-for="about in abouts">{{about.name}}</div>
//error
vue.js:2532[Vue warn]: Cannot use v-for on stateful component root element because it renders multiple elements:
<div class="about" v-for="about in abouts">{{about.name}}</div>
vue.js:2532[Vue warn]: Multiple root nodes returned from render function. Render function should return a single root node. (found in component <About>)
You are mapping your Vuex state getters and action correctly. Your problem is something else as your error message states...
In your component template you can not use v-for directive on a root element. For example this is not allowed because your component can have multiple root elements:
<template>
<div class="about" v-for="about in abouts">{{about.name}}</div>
</template>
instead do it this way:
<template>
<div>
<div class="about" v-for="about in abouts">{{about.name}}</div>
</div>
</template>
** *fixed typo in template tag **