Where are functions places when working with slots in Vue? - vue.js

If component A is a parent to component B, where B has a slot. If A uses the slot with a button, where do you place the clickHandler, in A or in B?
Is both possible as long as you don't have both the same time? And if so, are there a best practice or does it depend on the situation?

You place it in A.
Slots are just cues for Vue to 'put the template here'. B doesn't know that you're rendering a button in that template. A determines that it's a button being rendered, and thus determines what happens when that button is pressed.
Technically, you could check what is being rendered in the slot from Component B, and apply a clickHandler to the slot if it's a button, as it's all just a render function under the hood. But for the sake of 'where do I put my function', that's generally too complex and rarely useful.

The child component could expose data to the parent one using scoped slot, in the parent we put some element which could have some events that have handlers defined in the parent component.
Example :
List
<template>
<div>
<ul>
<li v-for="(item, i) in items" :key="i">
<slot name="item" :item="item"></slot>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "list",
props: ["items"],
};
</script>
parent :
<template>
<list :items="items">
<template #item="{ item }">
<h3>{{ item }}</h3>
<span #click="deleteItem(item)">delete</span>
</template>
</list>
</template>
<script>
import List from "./List.vue";
export default {
data() {
return {
items: ["item 1", "item 2", "item 3"],
};
},
methods: {
deleteItem(item) {
console.log(item);
this.items = this.items.filter((_item) => _item !== item);
},
},
components: {
List,
},
};
</script>
LIVE DEMO

Related

vue component detect own index among siblings

Suppose that I have some Vue components with this relationship. CompA uses a slot.
<CompA>
<CompB />
<CompB />
<CompC />
</CompA>
If I wanted to know which numbered sibling each of these are I could hard code it like:
<CompA>
<CompB i="0" />
<CompB i="1" />
<CompC i="2" />
</CompA>
...while defining an i prop on the definitions of both CompB and CompC.
Is there any method for these components to automatically detect what its own index is in the list of siblings which belong to its parent so that I do not need to hard code these indexes as props? Something that perhaps uses the computed option?
Answers specific to Vue 3 are okay.
Edit
The use case here is for building reusable widgets which conform to ARIA specifications where ARIA attributes need to reference unique IDs.
Example:
<TabContainer tabsWidgetUnique="Tabswidget-1">
<template v-slot:buttons="{ slotProps }">
<TabButton :index="0" :slotProps="slotProps">Button A</TabButton>
<TabButton :index="1" :slotProps="slotProps">Button B </TabButton>
<TabButton :index="2" :slotProps="slotProps">Button C </TabButton>
</template>
<template v-slot:panels="{ slotProps }">
<TabPanel :index="0" :slotProps="slotProps"> Panel A </TabPanel>
<TabPanel :index="1" :slotProps="slotProps"> Panel B </TabPanel>
<TabPanel :index="2" :slotProps="slotProps"> Panel C </TabPanel>
</template>
</TabContainer>
In this case I am manually setting the index of each so that in the components I can say:
<template>
<div
class="tab-panel"
v-show="index === activeTab"
tabindex="0"
:id="tabsWidgetUnique + '-panel-' + index"
:aria-labelledby="tabsWidgetUnique + '-button-' + index"
>
<slot />
</div>
</template>
I don't have any direct answer to your question, but I can offer you a different, and more practical approach.
<template>
<TabContaier :data="tabData" :widget="1" />
</template>
<script>
data() {
return {
tabData: {
buttons: [
{ id: 1, label: Button A},
{ id: 2, label: Button B},
],
panels: [
{ id: 1, content: "<h1>Title 1</h1><p>your content...</p>"},
{ id: 2, content: "<h1>Title 2</h1><p>your content...</p>"},
]
}
}
}
</script>
Tab.vue
<template>
<div class="tab-container">
<div class="buttons">
<TabButton
v-for="button in tabData.buttons"
:key="button.id"
#click="activeTab = button.id"
>{{ button.label }}</TabButton>
</div>
<div class="pabels">
<TabPanel
v-for="panel in tabData.panels"
:key="panel.id"
:class="{'is-visible': activeTab == pabel.id}"
:content="panel.content"
>
</div>
</div>
</template>
<script>
data() {
props: {
tabData: {
type: Object,
default: {}
},
widget: {
type: Number,
default: null
}
},
return {
avtiveTab: 1
}
}
</script>
TabPabel.vue
<div class="panel" v-html="content"></div>
Let me know, if you have any question.
If you don't declare a passed attribute as prop in child component it will be included in the $attrs property like :
this.$attrs.i
in option API or in composition one like:
setup(props,{attrs}){
//then here you could use attrs.i
}

Vues - How to use v-for and scoped slots in render function

I have a component that renders navigation links. It's based on scopedSlots feature. The component is working fine, but I'd like to improve it. This is how it's currently used:
<horizontal-navigation :items="items" #default="{ item }">
<navigation-item :item="item"></navigation-item>
</horizontal-navigation>
Array of items looks like this:
[
{ path: '/about', title: 'About' },
{ path: '/', title: 'Home' }
]
Here's HorizontalNavigation component template:
<template>
<div class="horizontal-navigation">
<slot v-for="(item, index) in items" :key="index" :item="item"></slot>
</div>
</template> -->
Here's NavigationItem component template
<template>
<router-link class="navigation-item" :path="item.path">{{ item.title }}</router-link>
</template>
I'm trying to replace HorizontalNavigation component's template with a render function, because I want to make this component to render links with a default styling when no slot content is provided.
Here's where I'm stuck:
render () {
const options = {
class: 'horizontal-navigation'
}
let children = []
if (this.$slots.default) {
children = this.$slots.default({ items: this.items }) // NEED TO ITERATE ITEMS TO CREATE INSTANCES OF A COMPONENT PASSED TO DEFAULT SLOT
}
return h('div', options, children)
}
The closest thing I found in the docs is this:
render() {
if (this.items.length) {
return Vue.h('ul', this.items.map((item) => {
return Vue.h('li', item.name)
}))
} else {
return Vue.h('p', 'No items found.')
}
}
Any advice?
In the template try to use conditional rendering to render the slot or the fallback content :
<div class="horizontal-navigation">
<template v-for="(item, index) in items" >
<template v-if="$slots.default">
<slot :item="item"></slot>
</template>
<template v-else>
<router-link class="navigation-item" :path="item.path">{{ item.title }}</router-link>
</template>
</template>
</div>

VueJS component ref component is not accessible at all

I don't understand the refs when using in Vue component. It is not working properly.. I have two files
show.vue
<template>
<div>
<b-container fluid class="bg-white" v-if="$refs.chart">
<b-row class="topTab types">
<b-col
:class="{ active: currentTab === index }"
v-for="(tabDisplay, index) in $refs.chart.tabDisplays"
:key="index"
>
<router-link
:to="{ query: { period: $route.query.period, tab: index } }"
>
{{ tabDisplay }}
</router-link>
</b-col>
</b-row>
</b-container>
<component v-bind:is="currentGame" ref="chart" />
</div>
</template>
<script>
export default {
computed: {
currentGame() {
return () =>
import(
`#/components/Trend/example/Charts/${this.group}/${this.id}/Base.vue`
);
},
}
};
</script>
Base.vue
<template>
<div>
dadsas
</div>
</template>
<script>
export default {
data: function() {
return {
tabDisplays: {
1: "example1",
2: "example2",
3: "example3",
4: "example4"
}
};
}
};
</script>
Take note that the second file renders properly showing the dasdas but the $refs.chart.tabDisplays is not. It will only show when I change something inside the <script> tag like adding 5: "example5" in the tabDisplays data then if I refresh it will be gone again. Basically, I just want to access the computed property of my child component. I am very aware I can use vuex but I want to try accessing a component's computed property via ref. What is wrong with my $.refs.chart?
As I noted in my comment, refs are only populated after rendering, so you won't have access to them during rendering. This is mentioned in the docs, see https://v2.vuejs.org/v2/api/#ref. The child component doesn't exist at the point you're trying to access it. The rendering process is responsible for creating the child components, it all gets a bit circular if you try to access them during that rendering process.
It looks like you've already made several key design decisions here about how to structure your application, such as component boundaries and data ownership, and those decisions are making it difficult to get where you want to be. It's not easy to make concrete suggestions about how to fix that based purely on the code provided.
So instead I will attempt to suggest a minimal change that should fix the immediate problem you're having.
To access the property of the child you're going to need the parent component to render twice. The first time it will create the chart and the second time it will have the relevant property available. One way to do this would be to copy the relevant property to the parent after rendering.
<template>
<div>
<b-container fluid class="bg-white" v-if="tabDisplays">
<b-row class="topTab types">
<b-col
:class="{ active: currentTab === index }"
v-for="(tabDisplay, index) in tabDisplays"
:key="index"
>
<router-link
:to="{ query: { period: $route.query.period, tab: index } }"
>
{{ tabDisplay }}
</router-link>
</b-col>
</b-row>
</b-container>
<component v-bind:is="currentGame" ref="chart" />
</div>
</template>
<script>
export default {
data () {
return { tabDisplays: null };
},
computed: {
currentGame() {
return () =>
import(
`#/components/Trend/example/Charts/${this.group}/${this.id}/Base.vue`
);
},
},
mounted () {
this.tabDisplays = this.$refs.chart.tabDisplays;
},
updated () {
this.tabDisplays = this.$refs.chart.tabDisplays;
}
};
</script>
In the code above I've introduced a tabDisplays property and that is then being synced with the child in mounted and updated. Within the template there's no reference to $refs at all.
While this should work I would repeat my earlier point that the 'correct' solution probably involves more significant changes. Syncing data up to a parent like this is not a normal Vue pattern and strongly suggests an architectural failure of some kind.
This is based on #skirtle's answer. I just made a few tweaks in his answer to produce what I really want
show.vue
<template>
<div>
<b-container fluid class="bg-white" v-if="chart">
<b-row class="topTab types">
<b-col
:class="{ active: currentTab === index }"
v-for="(tabDisplay, index) in chart.tabDisplays"
:key="index"
>
<router-link
:to="{ query: { period: $route.query.period, tab: index } }"
>
{{ tabDisplay }}
</router-link>
</b-col>
</b-row>
</b-container>
<component v-bind:is="currentGame" ref="chart" />
</div>
</template>
<script>
export default {
data: function() {
return {
chart: undefined
};
},
computed: {
currentGame() {
return () =>
import(
`#/components/Trend/高频彩/Charts/${this.group}/${this.id}/Base.vue`
);
},
},
updated() {
this.chart = this.$refs.chart;
}
};
</script>

Show on several elements in the same component in vuejs

Looping out a number of boxes within the same component in vuejs.
Each box has a button that reveals more text using v-on:click.
The problem is that all the boxes respond to this at the same time.
Is there a way to target the specific button being clicked if there are several buttons in a component?
Is there some way to isolate each button so they all dont activate at once?
<div class="filter-list-area">
<button #click="show =!show"> {{text}} </button>
<ul class="filter-list-item-area">
<li class="filter-list-item " v-for="(items, key) in packages">
<div>
<img class="fit_rating">
</div>
<div class="filter-list-item-info" >
<h3>{{items.partner_university}}</h3>
<p> Match: {{items.match_value}}</p>
<div v-for="(courses, key) in courses">
<transition name="courses">
<div class="courses2" v-show="show">
<p v-if="courses.Pu_Id === items.pu_Id">
{{courses.Course_name}}
</p>
</div>
</transition>
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
import testdata from '../App.vue'
export default {
data (){
return{
text: 'Show Courses',
testFilter: 'Sweden',
show: false
}
},
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
testuni: Array,
list: Array,
packages: Array,
courses: Array
},
methods:{
afunction(){
console.log(this.show);
},
changeText(){
if(this.show){
this.text = 'Hide Courses'
}
else{
this.text = "Show Courses"
}
}
},
mounted() {
this.afunction();
},
watch: {
show:
function() {
this.afunction()
this.changeText()
}
},
}
EDIT: I've created this before you posted the code example, but you could use same principle:
In your data add showMoreText, which will be used to track if show more data should be shown.
I would agree with #anaximander that you should use child components here
Simple example how to show/hide
<template>
<div>
<div v-for="(box, index) in [1,2,3,4,5]">
<div>
Box {{ box }} <button #click="toggleMore(index)">More</button>
</div>
<div v-show="showMoreText[index]">
More about box {{ box }}
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
showMoreText: {},
}
},
methods: {
toggleMore(index) {
/*
Adds a property to a reactive object, ensuring the new property is
also reactive, so triggers view updates.
https://vuejs.org/v2/api/#Vue-set
*/
this.$set(this.showMoreText, index, ! this.showMoreText[index])
}
}
}
</script>
This sounds like an ideal situation for a new child component, which will allow each instance of the new component to have its own separate state.
The child components can emit events to the parent, if cross-component communication is necessary.

When using conditional rendering, how do I prevent repeating the child components on each condition?

Scenario
I have a custom button component in Vue:
<custom-button type="link">Save</custom-button>
This is its template:
// custom-button.vue
<template>
<a v-if="type === 'link'" :href="href">
<span class="btn-label"><slot></slot></span>
</a>
<button v-else :type="type">
<span class="btn-label"><slot></slot></span>
</button>
</template>
You can see from the template that it has a type prop. If the type is link, instead of the <button> element, I am using <a>.
Question
You'll notice from the template that I repeated the child component, i.e. <span class="btn-label"><slot></slot></span> on both root components. How do I make it so that I won't have to repeat the child components?
In JSX, it's pretty straightforward. I just have to assign the child component to a variable:
const label = <span class="btn-label">{text}</span>
return (type === 'link')
? <a href={href}>{label}</a>
: <button type={type}>{label}</button>
In this situation, I would probably opt to write the render function directly since the template is small (with or without JSX), but if you want to use a template then you can use the <component> component to dynamically choose what you want to render as that element, like this:
Vue.component('custom-button', {
template: '#custom-button',
props: [
'type',
'href',
],
computed: {
props() {
return this.type === 'link'
? { is: 'a', href: this.href }
: { is: 'button', type: this.type };
},
},
});
new Vue({
el: '#app',
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<div id="app">
<custom-button type="button">Button</custom-button>
<custom-button type="submit">Submit</custom-button>
<custom-button type="link" href="http://www.google.com">Link</custom-button>
</div>
<template id="custom-button">
<component v-bind="props">
<span class="btn-label"><slot></slot></span>
</component>
</template>
Well you could always create a locally registered component...
// in custom-button.vue
components : {
'label' : {template : '<span class="btn-label"><slot></slot></span>'}
}