Vue 3 slots variable is empty for no reason - vue.js

Using the setup syntax, I started a new projet week ago and when I try to use "slots" in the template or even a computed with useSlots. The slots variable is always empty even if the v-slots are working great (passing datas like I want).
<DatatableGeneric
title="Liste de clients (API)"
:headers="headers"
:data="apiResponse.results"
:loading="loading"
:serverItemsLength="apiResponse.count"
v-model:options="tableOptions"
#selected="updateSelected"
:show-select="showSelect"
sort-by="name"
sort-desc
must-sort
>
<template v-slot:test> TEST </template>
<template v-slot:item-actions="{ item }">
<slot name="item-actions" :item="item"></slot>
</template>
</DatatableGeneric>
In this code you can see that I have 2 named slots, "test" and "item-actions".
Child component:
slots? you here? {{ slots}}
slots? still not here? {{ testSlots }}
<slot name="test"> aaaaa </slot>
<tbody>
<tr v-for="item in computePaginatedData" :key="item.name">
<td
v-if="showSelect"
class="d-flex justify-center items-center align-center"
style="width: 100%"
>
<v-checkbox
:value="item"
color="primary"
v-model="computeSelected"
hide-details
class="align-center justify-center"
/>
</td>
<td v-for="header in headers" :key="header.value">
<slot :name="`item-${header.value}`" :item="item">
{{ get(item, header.value) }}
</slot>
</td>
</tr>
</tbody>
....
<script setup lang="ts">
import { ref, computed, reactive, onMounted, watch } from 'vue'
import { useSlots } from 'vue'
const slots = useSlots()
const testSlots = computed(() => {
return slots
})
.....
In the projet I see:
slots? you here? {} slots? still not here? {}
Something is wrong here in the projet but pretty sure it's not related to my code as the slots are working fine. They just aren't listed in the slots variable.
Also tried with "$slots" in the template

Well, it worked if I just loop on $slots with:
<template v-for "keyName in Object.keys($slots)" :key="kayName">
But on vue 2, I remember that when I print $slots I can see every slots inside.
Here in vue 3, I don't see anything.

Related

Correct way to mutate data in slotted child component

I want to create a table component made out of three parts.
The wrapper, the heads and the data.
While most of it works pretty well, I'm struggling with the order by functionality.
When the user clicks on a th tag, the data should be reordered and a little indicator should be shown.
The ordering works but the indicator doesn't.
The actual problem
I know that it's bad to mutate a property inside the child although it's defined in the parent. Since I use slots, I can't use the normal $emit.
Using the approach shown here brings me Uncaught (in promise) TypeError: parent is null and Unhandled error during execution of scheduler flush.
Although I already know that my current approach is wrong, I don't know how to do it right.
Googling around, I found keywords like writable computed props and scoped slots, but I can't put it together.
So what's the right way to realize two-way-binding in a slotted environment?
My Table.vue file
<template>
<div class="px-4 sm:px-6 lg:px-8">
<div class="mt-8 flex flex-col">
<div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
<Search v-if="searchable" :filters="props.filters"></Search>
<div class="inline-block min-w-full py-2 align-middle">
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<slot name="table-heads"></slot>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
<slot name="table-body"></slot>
</tbody>
</table>
<Pagination v-if="paginationLinks" :links="paginationLinks"></Pagination>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import Pagination from "#/Components/Tables/Pagination.vue";
import {provide} from "vue";
import Search from "#/Components/Tables/Search.vue";
import {Inertia} from "#inertiajs/inertia";
let name = "Table";
let props = defineProps({
paginationLinks: Array,
dataUrl: String,
filters: Object,
order: Object,
searchable: Boolean
});
provide('dataUrl', props.dataUrl);
provide('order', props.order);
</script>
<style scoped>
</style>
My TableHead.vue file
<template>
<th #click="orderByClicked" scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 cursor-pointer">
<div class="flex justify-between">
<slot></slot>
<span v-if="order.orderBy === props.orderKey">
<i v-if="order.orderDirection === 'asc'" class="fa-solid fa-chevron-up"></i>
<i v-if="order.orderDirection === 'desc'" class="fa-solid fa-chevron-down"></i>
</span>
</div>
</th>
</template>
<script setup>
import { inject } from "vue";
import { Inertia } from "#inertiajs/inertia";
let name = "TableHead";
let dataUrl = inject('dataUrl');
let order = inject('order');
let props = defineProps({
orderKey: String,
orderByClicked: Function
});
function orderByClicked() {
if (props.orderKey) {
if (order.orderBy === props.orderKey)
order.orderDirection = order.orderDirection === 'asc' ? 'desc' : 'asc';
else
order.orderDirection = "asc"
order.orderBy = props.orderKey;
Inertia.get(dataUrl, {orderBy: props.orderKey, orderDirection: order.orderDirection}, {
preserveState: true,
replace: true
});
}
}
</script>
<style scoped>
</style>
My TableData.vue file (just to be complete)
<template>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
<slot></slot>
</td>
</template>
<script setup>
let name = "TableData";
</script>
<style scoped>
</style>
putting it together
<Table :pagination-links="props.users.links" :data-url="'/users'" :searchable="true" :filters="props.filters" :order="props.order">
<template #table-heads>
<TableHead order-key="name">name</TableHead>
<TableHead order-key="email">email</TableHead>
<TableHead>Bearbeiten</TableHead>
</template>
<template #table-body>
<tr v-for="user in users.data" :key="user.id">
<TableData>{{ user.username }}</TableData>
<TableData>{{ user.email}}</TableData>
</tr>
</template>
</Table>
It is likely to be a reactivity problem.
Your indicators relies on the order property which you set at setup and seem to change correctly however I don't see anything to make it reactive for your template.
In Table.vue where you provide it, you might just need to import ref from Vue to make it reactive:
import { ref, provide } from 'vue'
// provide static value
provide('dataUrl', props.dataUrl);
// provide reactive value
const order = ref(props.order)
provide('order', order);
I realized that the error only occurred in combination with FontAwesome, so I looked further.
By inspecting the code, I found out that FontAwesome manipulates the DOM.
It should have been clear in hindsight but anyway...
The cause
The point is, that when you insert a tag like <i class="fa-solid fa-sort-up"></i> it's converted to <svg class="svg-inline--fa fa-sort-up" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sort-up" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" data-fa-i2svg=""><path fill="currentColor" d="M182.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8H288c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z"></path></svg>
That's fine as long as you don't attach own attributes to the i tag because they get destroyed!
That happened to my v-if. Unfortunately it didn't silently vanish but it caused some weird errors.
The solution
Wrap any tags which are getting manipulated by a third party (in this case FontAwesome) within a suitable parent tag.
I came up with the following solution:
<span v-if="someCondition">
<i class="fa-solid fa-sort-up"></i>
</span>
Trivia
Interestingly this error didn't occur in https://sfc.vuejs.org for some reason. I couldn't reproduce it in any online tool. The components only crashed in my local dev environment.

Change vuetify simple-table to data-table

I have this simple table in Vuetify, without headers and only one column. How can I change it to vuetify v-data-table?
<v-simple-table>
<thead />
<tbody>
<tr
v-for="item in categories"
:key="item"
#click="handleClick"
>
<td>{{ item }}</td>
<v-switch />
</tr>
</tbody>
</v-simple-table>
categories is a simple array of strings. I want to change it to data-table in order to nicely handle clicking and selecting rows.
Check this codesandbox I made: https://codesandbox.io/s/stack-71617004-simple-to-v-data-table-bm2yn1?file=/src/components/Example.vue
Using body slot
You can use the body slot of the data table and use almost the same code you have in your simple table like this. This way you set up the handleClick function in the tr:
<v-data-table
:headers="headers"
:items="items"
hide-default-footer
hide-default-header
:items-per-page="-1"
:footer-props="{
itemsPerPageOptions: [-1],
}"
>
<template v-slot:body="{ items}">
<tbody>
<tr v-for="item in items" :key="item" #click="handleClick(item)">
<td align="left">{{item}}</td>
</tr>
</tbody>
</template>
</v-data-table>
Using item slot
Or you can use the item slot, like this. In this other way, the handleClick function is configured using the #click:row event of the data table.
If you try to use the item slot with your array of strings, it will work but you'll get some warnings in your console. Telling you that your data-table item slot expected an object and received an string. That's because v-data-table component expects to receive an array of objects.
To avoid this warning you can simply convert your array of string into a dummy array of objects using Array.prototype.map, and bind the computed property instead.
computed: {
myItemsTransformed() {
return this.items.map(item => ({ name: item }));
}
},
<v-data-table
:headers="headers"
:items="myItemsTransformed"
hide-default-footer
hide-default-header
:items-per-page="-1"
:footer-props="{
itemsPerPageOptions: [-1],
}"
#click:row="(item) => handleClick(item.name)"
>
<template #item.name="{ item }">
{{ item.name }}
</template>
</v-data-table>
Notice that in both examples I have used the props hide-default-footer, hide-default-header to hide the footer and header of the data table and also set the items-per-page to -1. To show all the items of the table and avoid the pagination.
You can change like this :
new Vue({
el: '#app',
vuetify: new Vuetify(),
data: () => ({
categoryList: ['Category 1', 'Category 2', 'Category 3'],
}),
computed: {
categoriesHeader() {
return [
{ text: "Name", value: "name" }
];
},
},
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.x/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify#2.5.0/dist/vuetify.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.min.css" rel="stylesheet">
<div id="app">
<v-app id="a">
<v-data-table :headers="categoriesHeader" :items="categoryList" item-key="id" class="elevation-1">
<template v-slot:[`item.name`]="{ item }">
{{ item }}
</template>
</v-data-table>
</v-app>
</div>

How to access slot props from the component used inside the slot?

so everything i can find about scoped slots and passing props dont work for my specific situation:
i have following component order:
Home/List/ListItem
now i desided to replace the ListItem with a slot and because i use the List in a other Component too, but in there i need the ListOptionsItem.
in my home component i did this:
<list
class="mapkeyList"
:content="displayList"
:filterbar="true"
#handleSelection="addSelection"
#delete="deleteElement"
#editItem="editItem"
header="Mapkeys"
:key="mapkeyListKey"
>
<list-item>
</list-item>
</list>
in my List component i have this:
<template>
<div>
<h2 v-if="header">{{header}}</h2>
<div class="listContainer" v-if="showedContent.length > 0">
<div v-for=" (item, index) in showedContent" :key="index">
<slot
:item="item"
:index="index"
:dragable="dragableItems"
#auswahl="auswahlHandle"
#deleteElement="deleteElement"
#editItem="editItem"
:dontShowButtons="dontShowButtons"
#dragStart="handleOverDragStart"
:dragItem="dragItem"
#position="$emit('emitPosition',item)"
:deaktivierbar="deaktivierbar"
>
</slot >
</div>
finaly the listItem and the listOptionsItem need to access this props in the slot:
listItem:
<template>
<div class= "flexSpaceBetween" #click="$emit('auswahl',item)">
<div class="textFett">
{{item[0]}}
</div>
<div>
{{item[1]}}
</div>
</div>
i dont want to write all the neccessarry code in the home component because the listOptionsItem does need more informations and more space to write code.
my goal was it to define in the Home component that i want the list to use the listItem component and in the Options component the list should use the listItemOptions component. in the future there could be added new listItem versions.
Any component used inside scoped slot has no implicit access to the slot props. To make them available inside the component, you must pass it down to that component as props explicitly...
<list
class="mapkeyList"
:content="displayList"
:key="mapkeyListKey">
<template v-slot:default="{ item }">
<list-item :item="item">
</list-item>
</template>
</list>
If you have a lot of props/events you want to pass along, the ability of both v-bind and v-on to take an object as an argument is very useful because you can pass all the data and event handlers at the same time:
// List component
<template>
<div>
<h2 v-if="header">{{header}}</h2>
<div class="listContainer" v-if="showedContent.length > 0">
<div v-for=" (item, index) in showedContent" :key="index">
<slot :props="slotProps" :on="slotEventsHandlers"
</slot >
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
slotProps() {
return {
item: this.item,
dragable: this.dragableItems
}
},
slotEventsHandlers() {
return {
deleteElement: this.deleteElement,
dragStart: this.handleOverDragStart
}
}
}
}
</script>
And use it in parent:
<list
class="mapkeyList"
:content="displayList"
:key="mapkeyListKey">
<template v-slot:default="{ props, on }">
<list-item v-bind="props" v-on="on">
</list-item>
</template>
</list>

Condition on template with v-if using v-slot prop

I'm trying to make a condition to enable a named slot like this:
<template v-slot:item="{ item }" v-if="item.loading">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
</template>
My use case is a Vuetify datatable: each item has a "loading" property, and I'd like to activate "item" slot only if the row is loading ("item" slot is Slot to replace the default rendering of a row)
The error is that item is undefined in the v-if, which seems logic : item is only defined for template children tag.
Is there a way to solve this problem?
You can filter the items that you pass to the datatable with a computed property.
Can you just not swap element based on loading ?
Vue.config.devtools = false;
Vue.config.productionTip = false;
var app = new Vue({
el: '#app',
data: {
items: [{data : "", loading: true}, {data : "Some data", loading: false}]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="item in items">
<div>
<div v-if="item.loading">
Loading...
</div>
<div v-else>
{{item.data}}
</div>
</div>
</div>
</div>
I had a similar problem, and I solved it in Vuetify 2 by importing VDataTable/Row as 'v-data-table-row', and using it to render 'regular' table rows, and for custom rows I used my own template.
JavaScript
import Row from 'vuetify/lib/components/VDataTable/Row.js';
export default {
components: { 'v-data-table-row': Row },
data() {
return {
currentItemName: 'Ice cream sandwich'
}
}
// headers, items, etc...
}
HTML
<template v-slot:item="{ item }">
<tr v-if="item.name == currentItemName" class="blue-grey lighten-4">
<td>Custom prefix - {{ item.name }}</td>
<td colspan="2">{{ item.calories }} - Custom suffix</td>
</tr>
<v-data-table-row v-else :headers="headers" :item="item">
<template
v-for="(index, name) in $scopedSlots"
v-slot:[name.substr(5)]="data"
>
<slot
v-if="name.substr(0, 5) === 'item.'"
:name="name"
v-bind="data"
></slot>
</template>
</v-data-table-row> </template
You can check out working example here.
You can just put the v-if on the child element
<template #item="{ item }">
<v-progress-circular
v-if="item.loading"
color="primary"
indeterminate
></v-progress-circular>
</template>

Passing scoped slots through multiple components

I have 2 components. One is a list component (list.vue). The other takes a list and wraps it with other features(searchandlist). Now, from the page.vue file I would like to call SearchAndList, giving it the list, and the render props. However, I cant get the dynamic data to show, only static.
ListItems.vue
<span>
<div v-for="item in items" :key="item.id">
<slot v-bind="item"></slot>
</div>
SearchAndList.vue
<div class="clients-list">
<div class="table-responsive">
<table class="table table-striped table-hover">
<list-items :items="items">
<slot name="row"></slot>
</list-items>
</table>
</div>
</div>
page.vue
<template>
<search-and-list :items="items">
<tr slot="row">
Hi
</tr>
</search-and-list>
</template>
<script>
import SearchAndList from '../components/base/SearchAndList'
export default {
components: {
SearchAndList
},
data() {
return {
items: [
{ id: 10, name: 'Marc' },
{ id: 11, name: 'Bob' },
{ id: 12, name: 'George' }
]
}
}
}
</script>
When using this, I get Hi listed out 3 times as exprected. However, when changing this to:
<search-and-list :items="items">
<tr slot="row" slot-scope="item">
{{ item.name}}
</tr>
</search-and-list>
I do get "Duplicate presence of slot "default" found in the same render tree - this will likely cause render errors." in the console, however, even giving the default slot a name=list, the error is the same, but default is not list.
Im sure there is something simple that I am missing. Any guidance would be great.
EDIT:
I have a child component () that exposes an { item } to its parent (). However, I would like to access { item } in the grand-parent (page.vue).
There is 2 missing bit in your configuration.
Expose you item in the List.vue:
<span>
<div v-for="item in items" :key="item.id">
<slot :item="item"></slot>
</div>
Then you need to bind the name in your SearchAndList.vue.
So try:
<div class="clients-list">
<div class="table-responsive">
<table class="table table-striped table-hover">
<list-items :items="items">
<slot scope-slot="{ item }" :name="item.name"></slot>
</list-items>
</table>
</div>