Vue does not get template ref of vNode-rendered elements array with v-for - vue.js

I'm creating a library and I do renderless components that just renders whatever is in slots. For the purpose of demonstration, I added dummy VNodes array instead.
<template>
<component
v-for="vnode of vnodes"
:key="vnode.toString()"
:is="vnode"
ref="elements"
/>
</template>
<script setup>
import { ref, h, onMounted } from "vue";
const elements = ref([]);
const vnodes = [
h(
"div",
{
class: "foo"
},
"Hello One!"
),
h(
"div",
{
class: "bar"
},
"Hello Two!"
)
];
onMounted(() => {
console.log(elements.value[0]);
// expected: First element of array (Element)
// reality: undefined
});
</script>
CodePen repro
In my use case, I fetch VNodes from slots and then rendering it via <component> in template. It renders properly, however I can't get the references to rendered elements at onMounted(). Does anyone know what's wrong with this approach, or is it a Vue bug?

I have tried it in codepen and I also think this is how you should do it in vue 3. (best practice as of Vue.js version 3)
Functional Template Refs
:ref="(el) => elements.push(el)"
instead of
ref="elements"
If you face issues with duplicates in the array you might have to reset the array in a vue lifecycle hooks

Related

Vue3 (Vite) directly access parent data from child component

I have a simple landing page using Vue3 Vite (SSG) without Vuex.
I need to pass a screenWidth value being watched in App.vue to a bunch of child components so that they change images depending on the user's screenWidth.
I could use props to pass this value, but it seems a bit cumbersome to write them for 8 child components, and to use composition data export or provide / inject is definitely overkill.
is there not a way to simply access a parent's data via something like instance.parent (didn't work), $parent.message (Vue2 way), etc from a child component?
// Parent:
data() {
return {
screenWidth: 123
}
}
// Child
<div v-if="$parent.screenWidth > 1200">
img...
</div>
EDIT: Solving this with props for now as no other (working) solution seems to be available in Vite for what used to be easy as pie in Vue2.
EDIT 2: It occurs to me now that using VueUse's built in useWindowSize might have been a good solution here.
Use v-model binding.
Parent component (assuming setup script):
<script setup lang='ts'>
import {ref} from 'vue';
const screenWidth = ref(720);
// use screenWidth as a regulat reactive variable here
</script>
<template>
<Child v-model="screenWidth" />
</template>
Child component:
<script setup lang="ts">
import {ref, watchEffect} from 'vue';
const props = defineProps<{
modelValue: number;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: number): void
}>();
const value = ref(props.modelValue);
watchEffect(() => value.value = props.modelValue);
function setValue(newValue: number) {
value.value = key;
emit('update:modelValue', value.value);
}
</script>
<template>
// Use `value` and `setValue` here
</template>

Vue Test Utils: how to pass Vuelidate validation rules to child components?

while trying to write a component test by using vue test utils, testing interaction between child components and stuff, I am stuck due to usage of Vuelidate from child components. Below is an example simplified:
// parent component code
<template>
<div>
<childA />
</div>
</template>
//childA code
<template>
<input v-model="value" />
</template>
<script>
...
validations: {
value: {
required
}
}
...
</script>
// parent component test
...
const wrapper = mount(MyParentComponent, {
...,
components: {
childA,
},
validations: {
value: required
},
...
})
I have tried to find a solution out there that I could mount (note here that I WANT to mount also the child components, so shallow-mount is not what I look for) the child component, with it's respective Vuelidate validation rules, but I still haven't found any solution.
Instead, my test gives me errors like:
Cannot read property `value` of undefined
which makes sense, since the test cannot access the child component's $v instance.
Has anyone achieved it so far?
For answering your question and after i've did some test i believe you missed the data part inside your mount
mount: render child components
shallowMount: doesn't render child components
MyParentComponent need to have in the options the structure of you're child component so this is why he is returning the error
And i saw that you're passing the import of your component directly but don't forget that your test folder is outside of your src folder
import ChildA from "#/components/ChildA";
will not work instead i propose to use absolute path directly to import your child component or use a configuration to resolve them
const wrapper = mount(MyParentComponent, {
data() {
return {
value: null
}
},
components: {
ChildA: () => import('../../src/components/ChildA'),
},
validations: {
value: required
},
})

Vue render function: include slots to child component without wrapper

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.

Nuxt render function for a string of HTML that contains Vue components

I'm trying to solve this for Nuxt
Codesandbox of a WIP not working: https://codesandbox.io/s/zw26v3940m
OK, so I have WordPress as a CMS, and it's outputting a bunch of HTML. A sample of the HTML looks like this:
'<h2>A heading tag</h2>
<site-banner image="{}" id="123">Slot text here</site-banner>
<p>some text</p>'
Notice that it contains a Vue component <site-banner> that has some props on it (the image prop is a JSON object I left out for brevity). That component is registered globally.
I have a component that we wrote, called <wp-content> that works great in Vue, but doesn't work in Nuxt. Note the two render functions, one is for Vue the other is for Nuxt (obviously this is for examples sake, I wouldn't use both).
export default {
props: {
html: {
type: String,
default: ""
}
},
render(h, context) {
// Worked great in Vue
return h({ template: this.html })
}
render(createElement, context) {
// Kind of works in Nuxt, but doesn't render Vue components at all
return createElement("div", { domProps: { innerHTML: this.html } })
}
}
So the last render function works in Nuxt except it won't actually render the Vue components in this.html, it just puts them on the page as HTML.
So how do I do this in Nuxt? I want to take a string of HTML from the server, and render it on the page, and turn any registered Vue components into proper full-blown Vue components. Basically a little "VueifyThis(html)" factory.
This was what worked and was the cleanest, thanks to Jonas Galvez from the Nuxt team via oTechie.
export default {
props: {
html: {
type: String,
default: ""
}
},
render(h) {
return h({
template: `<div>${this.html}</div>`
});
}
};
Then in your nuxt.config.js file:
build: {
extend(config, ctx) {
// Include the compiler version of Vue so that <component-name> works
config.resolve.alias["vue$"] = "vue/dist/vue.esm.js"
}
}
And if you use the v-html directive to render the html?
like:
<div v-html="html"></div>
I think it will do the job.
Here's a solution on codesandbox: https://codesandbox.io/s/wpcontent-j43sp
The main point is to wrap the dynamic component in a <div> (so an HTML tag) in the dynamicComponent() template, as it can only have one root element, and as it comes from Wordpress the source string itself can have any number of top level elements.
And the WpContent component had to be imported.
This is how I did it with Nuxt 3 :
<script setup lang="ts">
import { h } from 'vue';
const props = defineProps<{
class: string;
HTML: string
}>();
const VNode = () => h('div', { class: props.class, innerHTML: props.HTML })
</script>
<template>
<VNode />
</template>
There was not need to update nuxt.config.ts.
Hopefully it will help some of you.
I made some changes to your codesandbox. seems work now https://codesandbox.io/s/q9wl8ry6q9
Things I changed that didn't work:
template can only has one single root element in current version of Vue
v-bind only accept variables but you pass in a string.

In a functional component, how can I perform a pure transformation of props before rendering them?

I'm quite new to Vue, and a problem I'm facing right now is defining a functional Vue component that represents an individual grid tile.
This component renders as a single div element. It takes two props representing its x/y position in the grid and needs to use these two props to figure out the CSS classes to apply to the div. Essentially, I just need a way to run these props through a pure function that produces a classObject which I can then v-bind to the class attribute of the div.
Below is the template and component logic I've defined in GridSquare.vue:
<template functional>
<div :class="classObject(props)"></div>
</template>
<script lang="ts">
import Vue from "vue";
import Component from "vue-class-component";
<script lang="ts">
export default {
props: {
xIndex: Number,
yIndex: Number
},
methods: {
classObject(props) {
const { xIndex, yIndex } = props;
return {
normal: true,
"thick-left": xIndex % 10 === 0,
"thick-top": yIndex % 10 === 0,
"thick-right": (xIndex + 1) % 10 === 0,
"thick-bottom": (yIndex + 1) % 10 === 0
};
}
}
};
</script>
However, this doesn't work: all I see is blank space where the div elements should be, and errors in my console like this:
vue.runtime.esm.js?ff9b:1737 TypeError: _vm.classObject is not a function
To summarise, I'm just looking for a way to process props into a different object and then making use of that in my template.
This is not possible right now. You can track this related issue.
You'll have to write the render function manually.