Vue 3 How to pass slots through multiple components - vue.js

I'm trying to make a huge table reusable throughout my project with specified cells.
I want to pass my different cell components into my table component.
But I can't figure out how to pass a named slot through more than one child.
I'm using Vue v3.2.45
I'm expected to be able to use the slotProps of my named slot cell in App.vue
Like I did with id with the named slot test
I made a playground to make myself clear here
// App.vue
<script setup lang="ts">
import { ref } from 'vue'
import { ITable } from './types.ts'
import Table from './Table.vue'
const table = ref<ITable>({
tree: [
{ data: [{ value: '0' }, { value: '1' }, { value: '2' }, { value: '3' }] },
{ data: [{ value: '0' }, { value: '1' }, { value: '2' }, { value: '3' }] },
],
})
</script>
<template>
<Table :table="table">
<template #cell="{ cell }">
Hello {{ cell.value }}
</template>
<template #test="{ id }">
<div class="row">Row above is {{ id }}</div>
</template>
</Table>
</template>
// Table.vue
<template>
<div class="table">
<template v-for="(row, rowI) in table.tree" :key="rowI">
<Row :row="row" />
<slot name="test" :id="rowI" />
</template>
</div>
</template>
// Row.vue
<template>
<div class="row">
<div class="cell" v-for="(cell, cellI) in row.data" :key="cellI">
<slot name="cell" :cell="cell" />
</div>
</div>
</template>

You can expose the cell slot in your Table component and use it in App.vue
<!-- Table.vue -->
<template>
<div class="table">
<template v-for="(row, rowI) in table.tree" :key="rowI">
<Row :row="row">
<template #cell="{ cell }">
<slot name="cell" :cell="cell"></slot>
</template>
</Row>
<slot name="test" :id="rowI" />
</template>
</div>
</template>

Related

Vue 3 v-model not working with custom input component in Nuxt.js

I am trying to use a custom Vue input component with a v-model, but the value is not updating in the parent component. I am using Vue 3 with Nuxt.js.
Here is my custom input component:
<template>
<input
type="text"
:value="modelValue"
#input="$emit('update:modelValue', $event.target.value)"
:placeholder="placeholder"
class="border border-gray-300 rounded-lg w-full p-2 text-black m-1"
/>
</template>
<script setup>
const props = defineProps({
modelValue: String,
placeholder: String,
});
const emit = defineEmits(["update:modelValue"]);
</script>
<script>
export default {
name: "MyInput",
};
</script>
And here is how I am using it in the parent component:
<template>
<div>
<MyInput v-model="inputValue" placeholder="Enter a value" />
</div>
</template>
<script>
import MyInput from "./MyInput.vue";
export default {
name: "MyParentComponent",
components: {
MyInput,
},
data() {
return {
inputValue: "",
};
},
};
</script>
The inputValue data property is not being updated when I type in the input field. Can someone help me figure out what I'm doing wrong?
I have a Vue 3 project without Nuxt.js using this exact same code and it works there.
there is no mistake in your codes I used exact same code and it's working with no problem
but:
there is no need to import components (Nuxt supports auto import)
your file's structure should be like this:
|__components
|
|__MyInput.vue
|
|__MyParentComponent.vue
|__app.vue
MyInput.vue
<template>
<input
type="text"
:value="modelValue"
#input="$emit('update:modelValue', $event.target.value)"
:placeholder="placeholder"
class="border border-gray-300 rounded-lg w-full p-2 text-black m-1"
/>
</template>
<script setup>
const props = defineProps({
modelValue: String,
placeholder: String,
});
const emit = defineEmits(["update:modelValue"]);
</script>
the dev tools will show the name of the component. So no additional naming is necessary.
MyComponent.vue
<template>
<div>
<MyInput v-model="inputValue" placeholder="Enter a value" />
<p>{{ inputValue }}</p>
</div>
</template>
<script setup>
let inputValue = ref("");
</script>
app.vue
<template>
<MyComponent />
</template>

Executing js on slot

I'm a beginner in web development and I'm trying to help out friends restarting an old game. I'm in charge of the tooltip component but I hit a wall...
There are many Vue components and in a lot of them I want to call a child component named Tooltip, I'm using vue-tippy for easy configuration. This is the component:
<template>
<tippy class="tippy-tooltip">
<slot name='tooltip-trigger'></slot>
<template #content>
<slot name='tooltip-content'>
</slot>
</template>
</tippy>
</template>
<script>
import { formatText } from "#/utils/formatText";
export default {
name: "Tooltip",
methods:{
formatContent(value) {
if (! value) return '';
return formatText(value.toString());
}
},
}
</script>
In one of the other components I try to use the tooltip:
<template>
<a class="action-button" href="#">
<Tooltip>
<template #tooltip-trigger>
<span v-if="action.movementPointCost > 0">{{ action.movementPointCost }}<img src="#/assets/images/pm.png" alt="mp"></span>
<span v-else-if="action.actionPointCost > 0">{{ action.actionPointCost }}<img src="#/assets/images/pa.png" alt="ap"></span>
<span v-if="action.canExecute">{{ action.name }}</span>
<span v-else><s>{{ action.name }}</s></span>
<span v-if="action.successRate < 100" class="success-rate"> ({{ action.successRate }}%)</span>
</template>
<template #tooltip-content>
<h1>{{action.name}}</h1>
<p>{{action.description}}</p>
</template>
</Tooltip>
</a>
</template>
<script>
import Tooltip from "#/components/Utils/ToolTip";
export default {
props: {
action: Object
},
components: {Tooltip}
};
</script>
From here everything is fine, the tooltip is correctly displayed with the proper content.
The thing is, the text in the {{ named.description }} needs to be formatted with the formatContent content. I know I can use the props, the components would look like that:
Tooltip.vue:
<template>
<tippy class="tippy-tooltip">
<slot name='tooltip-trigger'></slot>
<template #content>
<h1 v-html="formatContent(title)" />
<p v-html="formatContent(content)"/>
</template>
</tippy>
</template>
<script>
import { formatText } from "#/utils/formatText";
export default {
name: "Tooltip",
methods:{
formatContent(value) {
if (! value) return '';
return formatText(value.toString());
}
},
props: {
title: {
type: String,
required: true
},
content: {
type: Array,
required: true
}
}
}
</script>
Parent.vue:
<template>
<a class="action-button" href="#">
<Tooltip :title="action.name" :content="action.description">
<template v-slot:tooltip-trigger>
<span v-if="action.movementPointCost > 0">{{ action.movementPointCost }}<img src="#/assets/images/pm.png" alt="mp"></span>
<span v-else-if="action.actionPointCost > 0">{{ action.actionPointCost }}<img src="#/assets/images/pa.png" alt="ap"></span>
<span v-if="action.canExecute">{{ action.name }}</span>
<span v-else><s>{{ action.name }}</s></span>
<span v-if="action.successRate < 100" class="success-rate"> ({{ action.successRate }}%)</span>
</template>
</Tooltip>
</a>
</template>
<script>
import Tooltip from "#/components/Utils/ToolTip";
export default {
props: {
action: Object
},
components: {Tooltip}
};
</script>
But I need to use a slot in the tooltip component because we'll have some "extensive" lists with v-for.
Is there a way to pass the data from a slot into a JS function?
If I understand you correctly, you're looking for scoped slots here.
These will allow you to pass information (including methods) from child components (the components with <slot> elements) back to the parents (the component(s) filling those slots), allowing parents to use chosen information directly in the slotted-in content.
In this case, we can give parents access to formatContent(), which will allow them to pass in content that uses it directly. This allows us to keep the flexibility of slots, with the data passing of props.
To add this to your example, we add some "scope" to your content slot in Tooltip.vue. This just means we one or more attributes to your <slot> element, in this case, formatContent:
<!-- Tooltip.vue -->
<template>
<tippy class="tippy-tooltip">
<slot name='tooltip-trigger'></slot>
<template #content>
<!-- Attributes we add or bind to this slot (eg. formatContent) -->
<!-- become available to components using the slot -->
<slot name='tooltip-content' :formatContent="formatContent"></slot>
</template>
</tippy>
</template>
<script>
import { formatText } from "#/utils/formatText";
export default {
name: "Tooltip",
methods: {
formatContent(value) {
// Rewrote as a ternary, but keep what you're comfortable with
return !value ? '' : formatText(value.toString());
}
},
}
</script>
Now that we've added some scope to the slot, parents filling the slot with content can use it by invoking a slot's "scope":
<!-- Parent.vue -->
<template>
<a class="action-button" href="#">
<Tooltip>
. . .
<template #tooltip-content="{ formatContent }">
<!-- Elements in this slot now have access to 'formatContent' -->
<h1>{{ formatContent(action.name) }}</h1>
<p>{{ formatContent(action.description) }}</p>
</template>
</Tooltip>
</a>
</template>
. . .
Sidenote: I prefer to use the destructured syntax for slot scope, because I feel it's clearer, and you only have to expose what you're actually using:
<template #tooltip-content="{ formatContent }">
But you can also use a variable name here if your prefer, which will become an object which has all your slot content as properties. Eg.:
<template #tooltip-content="slotProps">
<!-- 'formatContent' is now a property of 'slotProps' -->
<h1>{{ slotProps.formatContent(action.name) }}</h1>
<p>{{ slotProps.formatContent(action.description) }}</p>
</template>
If you still need the v-html rendering, you can still do that in the slot:
<template #tooltip-content="{ formatContent }">
<h1 v-html="formatContent(title)" />
<p v-html="formatContent(content)"/>
</template>

How can I write emit even to tell parent is icon is clicked in Vuejs

I am write input element with icon to show/hide password, how can I write event emit in v-icon to tell parent component that icon is click
Here is my child component BaseInputPassword
<div class="label">
{{ labelName }} <span v-if="required" class="required">※</span>
</div>
<div class="input_password">
<input
:type="type ? 'password' : 'text'"
:placeholder="placeholder"
/>
<v-icon #click="type =! type">{{ type ? "mdi-eye-off" : "mdi-eye" }}</v-icon>
</div>
</div>
</template>
<script>
export default {
props: ["labelName", "placeholder", "required", "type" ],
data() {
return {
};
},
methods: {
updateValue($event) {
this.$emit("input", $event);
},
}
}
</script>
Here is my parent component, im using three child component
<div class="input_area">
<BlockInputPassword labelName="現在のパスワード" required="true" :type="show1" v-model="password" ></BlockInputPassword>
<BlockInputPassword labelName="現在のパスワード" required="true" :type="show2" v-model="password" ></BlockInputPassword>
<BlockInputPassword labelName="新しいパスワードの確認" required="true" :type="show3" v-model="password"></BlockInputPassword>
</div>
</div>
<div class="footer">
<router-link to=""
><div class="btn_login btn_simple">
キャンセル
</div></router-link>
<router-link to=""
><div class="btn_login btn_simple status_color_4">
変更
</div></router-link>
</div>
</div>
</div>
</template>
<script>
import BlockInputPassword from "../components/common/BlockInputPassword"
export default {
components: {
BlockInputPassword,
},
name: "PasswordChange",
data() {
return {
password: '',
show1: false,
show2: false,
show3: false,
};
},
have you child component v-icon emit a custom event
<v-icon #click="$emit('icon-clicked')">
then from the parent component
<div class="input_area">
<BlockInputPassword #icon-click="doSomething" ...></BlockInputPassword>
...
</div>

Template slot in buefy carousel showing broken image

Hi i am newbie in vuejs and buefy. I wanted to do a carousel. However its already printing in the b-carousel-item but in template slot="indicators" it showing broken image. Can anyone help me i want to show the image also in the template slot
this is the code:
https://codesandbox.io/s/wonderful-gagarin-5wc8d?file=/src/App.vue
App.vue
<template>
<b-carousel :indicator-inside="false">
<b-carousel-item v-for="(item, i) in imgurl" :key="i">
<span class="image">
<img :src="getImgUrl(item)" />
</span>
</b-carousel-item>
<template slot="indicators" slot-scope="props">
<span class="al image">
<img :src="getImgUrl(props.item)" :title="props.item" />
</span>
</template>
</b-carousel>
</template>
<script>
export default {
data() {
return {
thumbs: null,
imgurl: [
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTE7e4ENLA4IRiYClFOOyc418WmdNTuWAAX_A&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQrMMLeNArJ-NmM-sRGGGr0ya8y0NSF4HZ8Aw&usqp=CAU",
],
};
},
methods: {
getImgUrl(value) {
this.thumbs = value;
return `${value}`;
},
},
};
</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>