How can I test nested slots in Vue - vue.js

I am trying to pass nested slots into my mounted component and I can;'t figure out the syntax, couldn't find anything in docs.
What I am trying to do is to add html element into drop-down slot
This is my test what I've tried but it looks to be wrong.
it('should render drop down', async () => {
const wrapper = mount(MyComponent, {
slots: {
utilities: `
<template slot="drop-down">
<input placeholder="Search bar" class="header-bar-mobile-drop-down-utility__example">
</template>
`
}
});
expect(wrapper.find(".header-bar-mobile-drop-down-utility").exists()).to.equal(true)
});

Related

How to find component, passed as slot into child component, in jest?

I have a component, that uses another 'wrapper' component and passing all of it's body as slots into it.
Something like that:
<template>
<WrapperComponent>
<Card />
</WrapperComponent>
</template>
Question is - how do I find this 'Card' component while testing parent component in jest?
Have tried like that, but failed:
describe('RegistrationModal', () => {
const wrapper = mount(RegistrationModal, {
localVue
});
test('Registration of private company', async () => {
const PrivateCard = wrapper.findAllComponents(Card).at(0);
await PrivateCard.trigger('click', ['private'] );
expect(wrapper.vm.action).toBe('private');
expect(wrapper.vm.step).toBe(1);
});
});

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.

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

Slot doesn't render well on child component VueJS

I'm trying to loop over a component, I fill up slot with some data but they are not rendering well.
Weird behaviors :
Data are displayed but not visible.
In chrome if i toggle the device toolbar in the debug panel, data are now visible.
Changing font-size in the debug panel make my data visible
When i put a Child component outside the loop, the looped ones are rendered well.
Snippet from my parent Component :
<li class="cards__item" v-for="(staffMember, index) in staff">
<card-profil>
<h3 slot="title">{{staffMember.name}}</h3>
</card-profil>
</li>
Snippet from my child Component :
<template>
<section class="card-profil">
<figure class="fig-card-profil">
<figcaption class="figcaption-card-profil">
<slot name="title"></slot>
</figcaption>
</figure>
</section>
</template>
I get my data this way in my parent component:
export default {
data: function () {
return {
staff: []
}
},
mounted () {
this.getStaff()
},
methods: {
getStaff: async function () {
const staff = await axios({ url: 'https://randomuser.me/api/?results=8' })
this.staff = staff.data.results
}
}
}
Is this problem of lifehook ? Do i have to use Scoped slot instead ? V-for issue ?
Thanks for sharing your thoughts.

How can I call method in other component on vue.js 2?

My first component like this :
<template>
...
</template>
<script>
export default {
...
methods: {
addPhoto() {
const data = { id_product: this.idProduct}
const item = this.idImage
this.$store.dispatch('addImage', data)
.then((response) => {
this.createImage(item, response)
});
},
}
}
</script>
If the method addPhoto called, it will call ajax and then it will get response ajax
I want to send response ajax and another parameter to method createImage. Method createImage is located in other component (second component)
My second component like this :
<template>
<div>
<ul class="list-inline list-photo">
<li v-for="item in items">
<div v-if="clicked[item]">
<img :src="image[item]" alt="">
<span class="fa fa-check-circle"></span>
</div>
<a v-else href="javascript:;" class="thumb thumb-upload"
title="Add Photo">
<span class="fa fa-plus fa-2x"></span>
</a>
</li>
</ul>
</div>
</template>
<script>
export default {
...
data() {
return {
items: [1,2,3,4,5],
clicked: [], // using an array because your items are numeric
}
},
methods: {
createImage(item, response) {
this.$set(this.clicked, item, true)
},
}
}
</script>
How can I run the createImage method on the second component and after that it can change the element in the second component?
No two components don't have a parent/child relation. They are all connected through the root vue instance. To access the root vue instance just call this.$root and you get the root instance.
....
.then((response) => {
this.$root.$emit('createImage', item, response)
});
and in the second component make the function that needs to be triggered
...
mounted() {
this.$root.$on('createImage', (item, response) => {
// your code goes here
})
}
It acts more like a socket. The event will be globally available, due to $root.
N.B. adding the vue instance to global window object is a bad practice
If these 2 components are siblings (no parent & child), then one solution is to use event bus.
General idea is to build a global event handler like so:
in main.js
window.Event = new Vue();
Then in your first component fire an event:
....
.then((response) => {
Event.$emit('createImage', item, response)
});
and in second component register a handler for listening to createImage event in mounted() hook:
...
mounted() {
Event.$on('createImage', (item, response) => {
// your code goes here
}
}
You can find more info by reading this turtorial and watching this screen cast.