VueJS component ref component is not accessible at all - vue.js

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>

Related

VUE3 use different v-for for the same component

I have a JobComponent.vue component where I fetch data from a VUEX Store. This component is used on two separate pages, first page Home.vue and second page AllJobs.vue.
In AllJobs.vue I used JobComponent.vue and everything is works fine, it's rendering all the jobs, but, here comes the problem...
In Home.vue I want to render only the last 5 jobs, so in store I make a getter that slice me only the latest 5 jobs.
How can I use this latestJobs from getters on the same component?
When I import the component in Home.vue page I can't use another v-for direct on the component...
here you can see my project structure and files
Home.vue
<template>
<div class="cards-container">
<JobComponent />
</div>
</template>
JobComponent.vue
<template>
<div v-for="job in allJobs" :key="job.id" class="card">
<div class="position">{{ job.position }}</div>
<div class="department">{{ job.department }}</div>
<div class="location">
<span class="material-symbols-outlined">location_on</span>
{{ job.location }}
</div>
<span class="material-symbols-outlined right-arrow">arrow_right_alt</span>
<span #click="deleteJob(job.id)" class="material-symbols-outlined right-arrow">delete</span>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
export default {
methods: {
...mapActions(['fetchJobs', 'deleteJob']),
},
computed: mapGetters(['allJobs']),
created() {
this.fetchJobs();
}
}
</script>
store.js (vuex)
const getters = {
allJobs: (state) => state.jobs,
latestJobs: (state) => {
const response = state.jobs.slice(0, 5);
return response;
}
};
Your component should be as independent as possible from the store. It's role is to display what ever is provided so it could be reused as you want, using props :
JobComponent.vue
<template>
<div class="card">
<div class="position">{{ position }}</div>
<div class="department">{{ department }}</div>
<div class="location">
<span class="material-symbols-outlined">location_on</span>
{{ location }}
</div>
<span class="material-symbols-outlined right-arrow">arrow_right_alt</span>
<span #click="$emit('deleteJob', id)" class="material-symbols-outlined right-arrow">delete</span>
</div>
</template>
<script>
export default {
props: {
id: string,
position: string,
department: string,
location: string
}
}
</script>
In this component you only display the provided data, and leave the responsibility of the parent component to choose how many components to display.
Home.vue
<template>
<div class="cards-container">
<JobComponent v-for="job in jobs" :key="job.id" :id="job.id" :position="job.position" :department="job.department" :location="job.location" #delete-job="deleteJob" />
</div>
</template>
<script>
export default {
created() {
this.$store.dispatch('fetchJobs')
},
computed: {
jobs() {
return this.$store.getters['latestJobs'] // Or allJobs, just make sure your getter returns an array even if no jobs are loaded yet.
}
},
methods: {
deleteJob() {
// Your logic for job delete
}
}
}
</script>

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
}

Vue2 How to use nested <template> tags?

I'm using Vue2's latest version.
I have 2 components; 1st component:
<stack>
<template #first>
<template #cell(details)="row">
{{ row.details ? "Hide" : "Show" }}
</template>
</template>
</stack>
2nd component:
<template>
<div>
<slot name="first"></slot>
</div>
</template>
<script>
export default {
data() {
return {
row: {
name: 'Test',
details: true
}
}
},
}
</script>
I want to simply insert the contents of the <template #first> from the 1st component into the slot in the 2nd component while maintaining everything inside the <template #first> tag.
So the desired outcome would look like this; the slot would be replaced with the contents of the <template #first> tag:
<template>
<div>
<template #cell(details)="row">
{{ row.details ? "Hide" : "Show" }}
</template>
</div>
</template>
<script>
export default {
data() {
return {
row: {
name: 'Test',
details: true
}
}
},
}
</script>
However, the 1st component does not work like this. It says that I cannot have a template inside a template. Is this actually possible what I'm trying to achieve here? If yes, how?

How can I set custom template for item in list and than use it inside `v-for` loop?

What I want to achieve is something like:
<li v-for="(item, index) in items" :key="index>
<div v-if="item.Component">
<item.Component :value="item.value" />
</div>
<div v-else>{{ item.value }}</div>
</li>
But anyway I don't like at all this solution. The idea of defining Component key for an item in items list is hard to maintain since at least it is hard to write it in template-style way (usually we are talking about too long HTML inside). Also I don't like to wrap item.Component inside div.
data() {
return {
list: [{
value: 'abc',
Component: {
props: ['value'],
template: `123 {{ value }} 312`
}
}]
};
}
Does anyone know the best-practice solution for this and where Vue describes such case in their docs?
You can use Vue's <component/> tag to dynamically set your component in your list.
<li v-for="(item, index) in items" :key="index>
<component v-if="item.Component" :is="item.Component" :value="item.value"></component>
<div v-else>{{ item.value }}</div>
</li>
<script>
...,
data: () => ({
list: [{
value: 'abc',
Component: {
props: ['value'],
template: `<div>123 {{ value }} 312</div>` // must be enclosed in a element.
}
}]
})
</script>
You can also import a component too so you can create a new file and put your templates and scripts there.
Parent.vue
<script>
import SomeComponent from "#/components/SomeComponent.vue"; //import your component here.
export default {
data() {
return {
list: [
{
value: "abc",
Component: SomeComponent // define your imported component here.
},
]
};
}
};
</script>
SomeComponent.vue
<template>
<div>123 {{ value }} 312</div>
</template>
<script>
export default {
name: "SomeComponent",
props: ["value"]
};
</script>
Here's a demo.

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>