Passing scoped slots through multiple components - vue.js

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>

Related

Vue 3 slots variable is empty for no reason

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.

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.

Has anyone came across this problem? [vue/no-multiple-template-root] The template root disallows 'v-for' directives.eslint-plugin-vue

It is actually a three problems in one:
[vue/no-multiple-template-root]
The template root disallows 'v-for' directives.eslint-plugin-vue
[vue/no-parsing-error]
Parsing error: Expected to be an alias, but got empty.eslint-plugin-vue
[vue/valid-v-for]
Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive.eslint-plugin-vue
Can anyone help me please I am so fed with searching online for it everywhere
enter code
<template>
<div class="post" v-for="post" in posts >
<div><strong>Title</strong>{{post.title}}</div>
<div><strong>Desctiption</strong>{{post.body}}</div>
</div>
</template>
<script>
export default{
data(){
return{
posts:[
{ id: 1, title: 'javascript', body: "the desctiption"},
{ id: 2, title: 'javascript2', body: "the desctiption"},
{ id: 3, title: 'javascript3', body: "the desctiption"},
]
}
}
}
Vue.js must have a single element at the root of the template. If you have av-for directive, when the DOM is populated there will be multiple <div> elements at the root, which Vue does not allow.
So you just need to add another <div> element to surround your v-for div.
Then, move the in posts within your quotes and add a :key
<template>
<div>
<div class="post" v-for="post in posts" :key="post.id">
<div><strong>Title</strong>{{post.title}}</div>
<div><strong>Desctiption</strong>{{post.body}}</div>
</div>
</div>
</template>
In vue two you should one root element, using v-for loop will render multiple elements in the template like :
<div class="post" >
...
</div>
<div class="post" >
...
</div>
to avoid this add an extra div and bind key to the post id :key="post.id":
<template>
<div class="posts">
<div class="post" v-for="post in posts" :key="post.id">
<div><strong>Title</strong>{{post.title}}</div>
<div><strong>Desctiption</strong>{{post.body}}</div>
</div>
</div>
</template>

How to fix "multiple root element" v-for error

I'm new to Vue.js and I'm still learning it. I bumped into this error which I don't know how to solve.
Here is my simple script:
<template>
<div v-for="item in array" :key="item.id">
{{item}}
</div>
</template>
<script>
export default {
data () {
return {
array: ['Lion','Bear','Fish','Bird']
}
}
}
</script>
And here is the error that come up:
Cannot use v-for on stateful component root element because it renders multiple elements.
1 |
2 | <div v-for="item in array" :key="item.id">
| ^^^^^^^^^^^^^^^^^^^^^
3 | {{item}}
4 | </div>
Is there anyone who knows how to fix this error?
Try also renaming variable from array to animalNames similar. And remove id from loop as there is not id element exist, instead you can use index
<template>
<div>
<div v-for="item,index in array" :key="index">
{{item}}
</div>
</div>
</template>
You must have a root tag. Try wrapping your looped div in another div.
<template>
<div>
<div v-for="item in array" :key="item.id">
{{item}}
</div>
</div>
</template>
<script>
export default {
data () {
return {
array: ['Lion','Bear','Fish','Bird']
}
}
}
</script>

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>