Vue 3 Composition API...how to replace getElementById - vue.js

I wrote the following in Vue 3 Composition API.
If you look on the "onMounted" I'm attaching an event listener to the window to keep the status box to the bottom of the screen. I'm using absolute position and bottom 0.
I'm wondering if I can make this more "Vue" by replacing the getElementById? I tried refs but it's not working.
Any suggestions or should I leave well enough alone?
Thanks
<template>
<div>
<transition name="toast">
<div id="slide" class="slide-modal" v-if="statuses.length">
<div class="clear-notifications" #click='clearAllNotifications()'>Clear all notifications</div>
<div v-for="status in statuses" :key="status.id" class="status-div">
<div>{{ status.text }}</div>
<div #click="closeStatus(status)"><span class="-x10-cross"></span></div>
</div>
</div>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed, watch, onMounted } from "vue";
import { useStore } from "../model/DataStore";
export default defineComponent({
setup() {
const store = useStore();
const statuses = ref([]);
const statusMessage = computed(() => store.state.statusMessage);
function addStatus(newMessage) {
statuses.value.push({
id: statuses.value.length + 1,
text: newMessage
})
}
watch(statusMessage, (newValue: string, oldValue: string) => {
addStatus(statusMessage.value);
})
onMounted(() => {
window.addEventListener("scroll", function (e) {
let vertical_position = 0;
vertical_position = pageYOffset;
if(document.getElementById("slide")){
document.getElementById('slide').style.bottom = -(vertical_position) + 'px';
}
});
})
return {
store,
statuses,
addStatus
};
},
methods: {
clearAllNotifications() {
this.statuses = []
},
closeStatus(elm: any) {
const index = this.statuses.map((status) => status.id).indexOf(elm.id);
this.statuses.splice(index, 1);
}
}
})
</script>
and here's the slide modal style:
.slide-modal {
max-height: 200px;
width: 500px;
background-color: #f2f2f2;
color: #505050;
padding: 8px;
display: flex;
flex-direction: column;
gap: 8px;
overflow-x: hidden;
position: absolute;
bottom: 0;
}

The docs give a pretty simple example
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const root = ref(null)
onMounted(() => {
// the DOM element will be assigned to the ref after initial render
console.log(root.value) // <div>This is a root element</div>
})
return {
root
}
}
}
</script>
use ref() when creating
pass to template in return function
use ref="..." in template
and in onMounted, access via ref.value
so for your code it would be...
<template>
<div>
<transition name="toast">
<div ref="slideRef" class="slide-modal" v-if="statuses.length">
<div class="clear-notifications" #click='clearAllNotifications()'>Clear all notifications</div>
<div v-for="status in statuses" :key="status.id" class="status-div">
<div>{{ status.text }}</div>
<div #click="closeStatus(status)"><span class="-x10-cross"></span></div>
</div>
</div>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed, watch, onMounted } from "vue";
import { useStore } from "../model/DataStore";
export default defineComponent({
setup() {
const store = useStore();
const statuses = ref([]);
const slideRef = ref();
const statusMessage = computed(() => store.state.statusMessage);
function addStatus(newMessage) {
statuses.value.push({
id: statuses.value.length + 1,
text: newMessage
})
}
watch(statusMessage, (newValue: string, oldValue: string) => {
addStatus(statusMessage.value);
})
onMounted(() => {
window.addEventListener("scroll", function (e) {
let vertical_position = 0;
vertical_position = pageYOffset;
if(slideRef.value){
slideRef.value.style.bottom = -(vertical_position) + 'px';
}
});
})
return {
store,
statuses,
addStatus,
slideRef
};
},
})
</script>

Related

I'm trying to make my button a route that shows a modal, but I want to stay on the same page

When I'm on the Home page, I want to click on the Login button in the Header and when I click, the Login modal is displayed, which is a separate component, but I want to stay on the Home page and not be redirected to another page.
Login component:
<script setup>
import { ref, onMounted } from 'vue'
import VueCookies from 'vue-cookies'
import axios from 'axios';
const LogInModalVisible = ref(false)
var baseURL = 'http://localhost:3000/'
var userURL = baseURL + "users";
let users = ref(null)
let email = ref(null)
let password = ref(null)
let emailError = ref(null)
let passwordError =ref(null)
let passwordValidation = ref(false)
onMounted(async () => {
try {
const res = await axios.get(baseURL + 'users');
users = res.data;
/* console.log(users) */
/* loginSubmit() */
} catch (e) {
console.error(e)
}
})
async function loginSubmit()
{
if(email.lenght == 0)
{
this.emailError = "Field is empty"
}
else
{
this.emailError = null
}
if(password.length == 0)
{
this.passwordError = "Field is empty!";
}
else if(password.length>0 && password.length<8)
{
this.passwordError = "Password is too short!"
}
else
{
this.passwordError = null
}
/* const res = await axios.get(userURL);
this.users = res.data; */
for (var i = 0; i <users.length; i++)
{
if (this.users[i].email == this.email.value)
{
if (this.users[i].password == this.password.value)
{
this.passwordValidation = true;
VueCookies.set('username', this.users[i].username, "120min");
VueCookies.set('email', this.email.value, "120min");
VueCookies.set('password', this.password.value, "120min");
VueCookies.set('id', this.users[i].id, "120min");
window.location.href = '/';
alert("Login successful");
}
}
}
if(this.passwordValidation == false){ this.passwordError = "Inccorect password or username!"}
}
</script>
<template>
<el-dialog v-model="LogInModalVisible" title="LogIn" width="50%" height="50%" center>
<el-form label-position='top' status-icon :label-width="80">
<el-form-item label="Email">
<el-input type="email" id='email' placeholder="Enter Email" v-model="email" />
<div class="input-message" v-if="emailError"><h6>{{emailError}}</h6></div>
</el-form-item>
<el-form-item label="Password">
<el-input type="password" id='password' placeholder="Enter Password" v-model="password" />
<div class="input-message" v-if="passwordError"><h6>{{passwordError}}</h6></div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button #click="LogInModalVisible = false">Cancel</el-button>
<el-button type="primary" #click="LogInModalVisible = false; loginSubmit()">
Confirm
</el-button>
</span>
</template>
</el-dialog>
</template>
<style>
</style>
Header component:
<script setup>
import {Sunny, Moon} from '#element-plus/icons-vue'
import { ref, onMounted } from 'vue'
import { RouterLink } from 'vue-router'
import Home from '../views/Home.vue'
import { pokeStore } from '../store/store'
import VueCookies from 'vue-cookies'
import axios from 'axios';
import Pokedex from './Pokedex.vue'
const pokedexVisible = ref(false)
const PokemonStore = pokeStore();
let allpokemons = []
async function GetAllPokemons() {
try {
let response = await PokemonStore.getPokemonData();
allpokemons.value = response.data.results;
let randomPokemon = allpokemons.value[Math.floor(Math.random() * 151) + 1]
console.log(randomPokemon)
} catch (error) {
throw error;
}
}
GetAllPokemons()
</script>
<template>
<el-header class="navbar">
<div class="navbar-content">
<div>
<router-link to="/" custom v-slot="{ navigate }">
<img #click="navigate" role="link" class="logo" src="/src/assets/images/logo.png" />
</router-link>
</div>
<el-space size="large">
<div>
<input class="search" type="text" placeholder="Search pokemon" />
</div>
<div>
<el-button link><el-icon :size="20">
<Sunny />
</el-icon></el-button>
<el-button link><el-icon :size="20">
<Moon />
</el-icon></el-button>
</div>
<div>
<el-button #click="pokedexVisible = true" class="pokedexBtn">Pokedex</el-button>
<!-- <router-link #click="pokedexVisible = true" to="/pokedex" class="nav-link">Pokedex</router-link> -->
</div>
<div>
<el-button #click="LogInModalVisible = true" text>LogIn</el-button>
<router-link to="/login" custom v-slot="{ navigate }">
<button #click="navigate" role="link">
Login
</button>
</router-link>
</div>
</el-space>
</div>
</el-header>
</template>
<style scoped>
.navbar {
background-color: whitesmoke;
padding: 5px 30px;
position: fixed;
top: 0;
left: 0;
right: 0;
opacity: 0.9;
}
.navbar-content{
align-items: center;
justify-content: space-between;
display: flex;
}
.logo{
width: 100%;
max-width: 50px;
display: block;
}
.search{
display: block;
padding: 5px;
border-radius: 4px;
font-size: 14px;
width: 100%;
background-color: transparent;
float: right;
}
.search::placeholder {
opacity: 0.5;
}
.navbar-right{
align-items: center;
justify-content: space-between;
}
.pokedexBtn{
background-color: black;
color: whitesmoke;
}
</style>
Index.js component:
import { createRouter, createWebHistory } from 'vue-router'
import Login from '../components/Login.vue'
import Home from '../views/Home.vue'
import Pokedex from '../components/Pokedex.vue'
import App from '/src/App.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/pokedex',
name: 'Pokedex',
component: Pokedex
},
{
path: '/login',
name: 'Login',
component: Login
},
]
})
export default router
When I'm on the Home page, I want to click on the Login button in the Header and when I click, the Login modal is displayed, which is a separate component, but I want to stay on the Home page and not be redirected to another page.
I haven't tested this but it would be something like this.
<script setup>
import {Sunny, Moon} from '#element-plus/icons-vue'
import { ref, onMounted } from 'vue'
import { RouterLink } from 'vue-router'
import Home from '../views/Home.vue'
import { pokeStore } from '../store/store'
import VueCookies from 'vue-cookies'
import axios from 'axios';
import Pokedex from './Pokedex.vue'
const pokedexVisible = ref(false)
const PokemonStore = pokeStore();
let allpokemons = []
async function GetAllPokemons() {
try {
let response = await PokemonStore.getPokemonData();
allpokemons.value = response.data.results;
let randomPokemon = allpokemons.value[Math.floor(Math.random() * 151) + 1]
console.log(randomPokemon)
} catch (error) {
throw error;
}
}
GetAllPokemons()
import { LoginModal } from 'somewhere/LoginModal.vue';
const showLoginModal = ref(false);
</script>
<template>
<el-header class="navbar">
<div class="navbar-content">
<div>
<router-link to="/" custom v-slot="{ navigate }">
<img #click="navigate" role="link" class="logo" src="/src/assets/images/logo.png" />
</router-link>
</div>
<el-space size="large">
<div>
<input class="search" type="text" placeholder="Search pokemon" />
</div>
<div>
<el-button link><el-icon :size="20">
<Sunny />
</el-icon></el-button>
<el-button link><el-icon :size="20">
<Moon />
</el-icon></el-button>
</div>
<div>
<el-button #click="pokedexVisible = true" class="pokedexBtn">Pokedex</el-button>
<!-- <router-link #click="pokedexVisible = true" to="/pokedex" class="nav-link">Pokedex</router-link> -->
</div>
<div>
<el-button #click.prevent="showLoginModal = true" text>LogIn</el-button>
</div>
</el-space>
</div>
</el-header>
<LoginModal v-if="showLoginModal">
</template>

VUE.JS 3 Changing boolean value of one sibling component from another

I have two components - component A and component B that are siblings.
I need to change the boolean value inside of Component-A from the Watcher in Component-B.
Component A code:
<template>
<div></div>
</template>
<script>
export default {
data() {
return {
editIsClicked: false,
}
}
}
</script>
Component B code:
<template>
<v-pagination
v-model="currentPage"
:length="lastPage"
:total-visible="8"
></v-pagination>
</template>
<script>
export default {
props: ["store", "collection"],
watch: {
currentPage(newVal) {
this.paginatePage(newVal);
// NEED TO TOGGLE VALUE HERE - when i switch between pages
},
},
},
};
</script>
The Vue Documentation proposes communicating between Vue Components using props and events in the following way
*--------- Vue Component -------*
some data => | -> props -> logic -> event -> | => other components
*-------------------------------*
It's also important to understand how v-model works with components in Vue v3 (Component v-model).
const { createApp } = Vue;
const myComponent = {
props: ['modelValue'],
emits: ['update:modelValue'],
data() {
return {
childValue: this.modelValue
}
},
watch: {
childValue(newVal) {
this.$emit('update:modelValue', newVal)
}
},
template: '<label>Child Value:</label> {{childValue}} <input type="checkbox" v-model="childValue" />'
}
const App = {
components: {
myComponent
},
data() {
return {
parentValue: false
}
}
}
const app = createApp(App)
app.mount('#app')
<div id="app">
Parent Value: {{parentValue}}<br />
<my-component v-model="parentValue"/>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
I have made a new playground. Hope it helps you now to understand the logic.
You can store data in the main Vue App instance or use a Pinia store for it.
But I would suggest you to start without Pinia to make your app simpler. Using Pinia will make your App much more complicated and your knowledge of Vue seems to be not solid enough for that.
const { createApp } = Vue;
const myComponentA = {
props: ['editIsClicked', 'currentPage'],
template: '#my-component-a'
}
const myComponentB = {
emits: ['editIsClicked'],
data() {
return {
currentPage: 1,
}
},
watch: {
currentPage(newVal) {
this.$emit('editIsClicked', newVal)
}
},
template: '#my-component-b'
}
const App = {
components: {
myComponentA, myComponentB
},
data() {
return {
editIsClicked: false,
currentPage: 1
}
},
methods: {
setEditIsClicked(val) {
this.editIsClicked = true;
this.currentPage = val;
}
}
}
const app = createApp(App)
app.mount('#app')
#app { line-height: 2; }
.comp-a { background-color: #f8f9e0; }
.comp-b { background-color: #d9eba7; }
<div id="app">
<my-component-a :edit-is-clicked="editIsClicked" :current-page="currentPage"></my-component-a>
<my-component-b #edit-is-clicked="setEditIsClicked"></my-component-b>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<script type="text/x-template" id="my-component-a">
<div class="comp-a">
My Component A: <br />editIsClicked: <b>{{editIsClicked}}</b><br/>
currentPage: <b>{{currentPage}}</b><br/>
</div>
</script>
<script type="text/x-template" id="my-component-b">
<div class="comp-b">
My Component B: <br />
<label>CurrentPage:</label> <input type="number" v-model="currentPage" />
</div>
</script>

How do i get a Vue Compsable to only work with a single target item in a list

I have a composable that takes a function and calls the function upon the long-press of a component that uses it(the composable). however, if I use it in a list of components, it seems to call the function on all the components instead of only the component/item I long-pressed in the list. How do I make it so that the function is only called on the target component/item?
The composable(/composables/useLongPress)
import { ref, onMounted, onUnmounted } from 'vue';
const longPress = (binding) => {
const isHeld = ref(false);
const activeHoldTimer = ref(null);
const onHoldStart = () => {
isHeld.value = true;
activeHoldTimer.value = setTimeout(() => {
if (isHeld.value) {
binding();
}
}, 1000);
};
const onHoldEnd = () => {
isHeld.value = false;
if (activeHoldTimer.value) {
clearTimeout(activeHoldTimer.value);
}
};
onMounted(() => {
window.addEventListener('mousedown', onHoldStart);
window.addEventListener('touchstart', onHoldStart);
window.addEventListener('click', onHoldEnd);
window.addEventListener('mouseout', onHoldEnd);
window.addEventListener('touchend', onHoldEnd);
window.addEventListener('touchcancel', onHoldEnd);
});
onUnmounted(() => {
window.removeEventListener('mousedown', onHoldStart);
window.removeEventListener('touchstart', onHoldStart);
window.removeEventListener('click', onHoldEnd);
window.removeEventListener('mouseout', onHoldEnd);
window.removeEventListener('touchend', onHoldEnd);
window.removeEventListener('touchcancel', onHoldEnd);
});
};
export default longPress;
The child component/item (/InvoiceListItem.vue)
<template>
<div class="grid grid-cols-2 py-4">
<div>
<p class="text-lg font-medium">{{ invoice.client }}</p>
<p class="mt-2 text-xs font-light text-gray-500">Due {{ invoice.due }}</p>
</div>
<div class="flex flex-col items-end">
<p class="text-lg font-bold">${{ invoice.amount }}</p>
<base-pill
:variant="invoice.status"
:label="capitalize(invoice.status)"
class="mt-2"
/>
</div>
</div>
</template>
<script setup>
import BasePill from '../../BasePill.vue';
import { capitalize } from '../../../utils';
import useLongPress from '../../../composables/useLongPress';
const props = defineProps({
invoice: {
type: Object,
required: true,
},
});
const log = () => console.log(props.invoice);
useLongPress(log);
</script>
<style lang="postcss" scoped></style>
The parent/list of components (/InvoiceList.vue)
<template>
<div class="divide-y">
<div v-for="(invoice, index) in invoices" :key="index">
<invoice-list-item :invoice="invoice" />
</div>
</div>
</template>
<script setup>
import InvoiceListItem from './InvoiceListItem.vue';
defineProps({
invoices: {
type: Array,
default: () => [],
},
});
</script>
<style lang="scss" scoped></style>
when I long press on one invoice item I am supposed to log the value of the one item but instead, I get the value of all items in the list

Vue 3 how to get information about $children

This my old code with VUE 2 in Tabs component:
created() {
this.tabs = this.$children;
}
Tabs:
<Tabs>
<Tab title="tab title">
....
</Tab>
<Tab title="tab title">
....
</Tab>
</Tabs>
VUE 3:
How can I get some information about childrens in Tabs component, using composition API? Get length, iterate over them, and create tabs header, ...etc? Any ideas? (using composition API)
This is my Vue 3 component now. I used provide to get information in child Tab component.
<template>
<div class="tabs">
<div class="tabs-header">
<div
v-for="(tab, index) in tabs"
:key="index"
#click="selectTab(index)"
:class="{'tab-selected': index === selectedIndex}"
class="tab"
>
{{ tab.props.title }}
</div>
</div>
<slot></slot>
</div>
</template>
<script lang="ts">
import {defineComponent, reactive, provide, onMounted, onBeforeMount, toRefs, VNode} from "vue";
interface TabProps {
title: string;
}
export default defineComponent({
name: "Tabs",
setup(_, {slots}) {
const state = reactive({
selectedIndex: 0,
tabs: [] as VNode<TabProps>[],
count: 0
});
provide("TabsProvider", state);
const selectTab = (i: number) => {
state.selectedIndex = i;
};
onBeforeMount(() => {
if (slots.default) {
state.tabs = slots.default().filter((child) => child.type.name === "Tab");
}
});
onMounted(() => {
selectTab(0);
});
return {...toRefs(state), selectTab};
}
});
</script>
Tab component:
<script lang="ts">
export default defineComponent({
name: "Tab",
setup() {
const index = ref(0);
const isActive = ref(false);
const tabs = inject("TabsProvider");
watch(
() => tabs.selectedIndex,
() => {
isActive.value = index.value === tabs.selectedIndex;
}
);
onBeforeMount(() => {
index.value = tabs.count;
tabs.count++;
isActive.value = index.value === tabs.selectedIndex;
});
return {index, isActive};
}
});
</script>
<template>
<div class="tab" v-show="isActive">
<slot></slot>
</div>
</template>
Oh guys, I solved it:
this.$slots.default().filter(child => child.type.name === 'Tab')
To someone wanting whole code:
Tabs.vue
<template>
<div>
<div class="tabs">
<ul>
<li v-for="tab in tabs" :class="{ 'is-active': tab.isActive }">
<a :href="tab.href" #click="selectTab(tab)">{{ tab.name }}</a>
</li>
</ul>
</div>
<div class="tabs-details">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "Tabs",
data() {
return {tabs: [] };
},
created() {
},
methods: {
selectTab(selectedTab) {
this.tabs.forEach(tab => {
tab.isActive = (tab.name == selectedTab.name);
});
}
}
}
</script>
<style scoped>
</style>
Tab.vue
<template>
<div v-show="isActive"><slot></slot></div>
</template>
<script>
export default {
name: "Tab",
props: {
name: { required: true },
selected: { default: false}
},
data() {
return {
isActive: false
};
},
computed: {
href() {
return '#' + this.name.toLowerCase().replace(/ /g, '-');
}
},
mounted() {
this.isActive = this.selected;
},
created() {
this.$parent.tabs.push(this);
},
}
</script>
<style scoped>
</style>
App.js
<template>
<Tabs>
<Tab :selected="true"
:name="'a'">
aa
</Tab>
<Tab :name="'b'">
bb
</Tab>
<Tab :name="'c'">
cc
</Tab>
</Tabs>
<template/>
If you copy pasted same code as me
then just add to the "tab" component a created method which adds itself to the tabs array of its parent
created() {
this.$parent.tabs.push(this);
},
My solution for scanning children elements (after much sifting through vue code) is this.
export function findChildren(parent, matcher) {
const found = [];
const root = parent.$.subTree;
walk(root, child => {
if (!matcher || matcher.test(child.$options.name)) {
found.push(child);
}
});
return found;
}
function walk(vnode, cb) {
if (!vnode) return;
if (vnode.component) {
const proxy = vnode.component.proxy;
if (proxy) cb(vnode.component.proxy);
walk(vnode.component.subTree, cb);
} else if (vnode.shapeFlag & 16) {
const vnodes = vnode.children;
for (let i = 0; i < vnodes.length; i++) {
walk(vnodes[i], cb);
}
}
}
This will return the child Components. My use for this is I have some generic dialog handling code that searches for child form element components to consult their validity state.
const found = findChildren(this, /^(OSelect|OInput|OInputitems)$/);
const invalid = found.filter(input => !input.checkHtml5Validity());
I made a small improvement to Ingrid Oberbüchler's component as it was not working with hot-reload/dynamic tabs.
in Tab.vue:
onBeforeMount(() => {
// ...
})
onBeforeUnmount(() => {
tabs.count--
})
In Tabs.vue:
const selectTab = // ...
// ...
watch(
() => state.count,
() => {
if (slots.default) {
state.tabs = slots.default().filter((child) => child.type.name === "Tab")
}
}
)
I had the same problem, and after doing so much research and asking myself why they had removed $children, I discovered that they created a better and more elegant alternative.
It's about Dynamic Components. (<component: is =" currentTabComponent "> </component>).
The information I found here:
https://v3.vuejs.org/guide/component-basics.html#dynamic-components
I hope this is useful for you, greetings to all !!
I found this updated Vue3 tutorial Building a Reusable Tabs Component with Vue Slots very helpful with explanations that connected with me.
It uses ref, provide and inject to replace this.tabs = this.$children; with which I was having the same problem.
I had been following the earlier version of the tutorial for building a tabs component (Vue2) that I originally found Creating Your Own Reusable Vue Tabs Component.
With script setup syntax, you can use useSlots: https://vuejs.org/api/sfc-script-setup.html#useslots-useattrs
<script setup>
import { useSlots, ref, computed } from 'vue';
const props = defineProps({
perPage: {
type: Number,
required: true,
},
});
const slots = useSlots();
const amountToShow = ref(props.perPage);
const totalChildrenCount = computed(() => slots.default()[0].children.length);
const childrenToShow = computed(() => slots.default()[0].children.slice(0, amountToShow.value));
</script>
<template>
<component
:is="child"
v-for="(child, index) in childrenToShow"
:key="`show-more-${child.key}-${index}`"
></component>
</template>
A per Vue documentation, supposing you have a default slot under Tabs component, you could have access to the slot´s children directly in the template like so:
// Tabs component
<template>
<div v-if="$slots && $slots.default && $slots.default()[0]" class="tabs-container">
<button
v-for="(tab, index) in getTabs($slots.default()[0].children)"
:key="index"
:class="{ active: modelValue === index }"
#click="$emit('update:model-value', index)"
>
<span>
{{ tab.props.title }}
</span>
</button>
</div>
<slot></slot>
</template>
<script setup>
defineProps({ modelValue: Number })
defineEmits(['update:model-value'])
const getTabs = tabs => {
if (Array.isArray(tabs)) {
return tabs.filter(tab => tab.type.name === 'Tab')
} else {
return []
}
</script>
<style>
...
</style>
And the Tab component could be something like:
// Tab component
<template>
<div v-show="active">
<slot></slot>
</div>
</template>
<script>
export default { name: 'Tab' }
</script>
<script setup>
defineProps({
active: Boolean,
title: String
})
</script>
The implementation should look similar to the following (considering an array of objects, one for each section, with a title and a component):
...
<tabs v-model="active">
<tab
v-for="(section, index) in sections"
:key="index"
:title="section.title"
:active="index === active"
>
<component
:is="section.component"
></component>
</app-tab>
</app-tabs>
...
<script setup>
import { ref } from 'vue'
const active = ref(0)
</script>
Another way is to make use of useSlots as explained in Vue´s documentation (link above).
Based on the answer of #Urkle:
/**
* walks a node down
* #param vnode
* #param cb
*/
export function walk(vnode, cb) {
if (!vnode) return;
if (vnode.component) {
const proxy = vnode.component.proxy;
if (proxy) cb(vnode.component.proxy);
walk(vnode.component.subTree, cb);
} else if (vnode.shapeFlag & 16) {
const vnodes = vnode.children;
for (let i = 0; i < vnodes.length; i++) {
walk(vnodes[i], cb);
}
}
}
Instead of
this.$root.$children.forEach(component => {})
write
walk(this.$root, component => {})
Many thanks #Urkle
In 3.x, the $children property is removed and no longer supported. Instead, if you need to access a child component instance, they recommend using $refs. as a array
https://v3-migration.vuejs.org/breaking-changes/children.html#_2-x-syntax

Error message when trying to pass data from component to page in Nuxt

I've created a component in Nuxt to get data from a Firestore database and would like to show that data in a page I created.
When I embed the component in a page I keep getting the error:
Property or method "provinces" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
Now, when I copy the code from the component in the page it works just fine, so I assume the problem is with passing the data between the component and the page.
Full code of the component in components/index.vue :
<template>
<section class="container">
<div class="index">
<div v-for="province in provinces" :key="province.id">
<div class="div_title">
<h2>{{ province.name_nl }}</h2>
</div>
</div>
</div>
</section>
</template>
<script>
// import { VueperSlides, VueperSlide } from 'vueperslides'
// import 'vueperslides/dist/vueperslides.css'
import firebase from 'firebase'
// import fireDb from '#/plugins/firebase.js'
export default {
name: 'Index',
components: {
// VueperSlides,
// VueperSlide
},
data: function() {
return {}
},
async asyncData() {
const moment = require('moment')
const date = moment(new Date()).format('YYYY-MM-DD')
const housesArray = []
const provincesArray = []
await firebase
.firestore()
.collection('provinces')
.orderBy('name_nl')
.get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
provincesArray.push(doc.data())
})
})
await firebase
.firestore()
.collection('houses')
.where('valid_until', '>', date)
.get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
housesArray.push(doc.data())
})
})
return {
provinces: provincesArray,
houses: housesArray
}
}
}
</script>
<style scoped>
div {
text-align: center;
}
h2 {
margin-top: 5vh;
margin-left: auto;
margin-right: auto;
width: 95vh;
}
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
p {
text-align: left;
}
li {
min-width: 100% !important;
margin-left: 0px;
text-align: left;
}
</style>
Page where I insert the component in pages/index.vue:
<template>
<v-layout column justify-center align-center>
<v-flex xs12 sm8 md6>
<div class="text-xs-center">
<logo />
<tabs />
<index />
</div>
</v-flex>
</v-layout>
</template>
<script>
import Logo from '~/components/Logo.vue'
import Tabs from '~/components/Tabs.vue'
import firebase from 'firebase'
import Index from '~/components/Index.vue'
export default {
components: {
Logo,
Tabs,
Index
},
data: function() {
return {}
}
}
</script>
I would expect the page to display the data that I retrieved when I import the component into the page but I keep getting the same error.
Should I be using the Nuxt store to transfer data between a component and a page or am I doing something else wrong?
The lifecycle hook asyncData is not know within Vue components. It's only known in Nuxt pages.
It's better to do the data request within your pages component and pass it as a property to your component:
pages/index.vue
<template>
<index :houses="houses" />
</template>
<script>
const delay = time => new Promise(resolve => setTimeout(resolve, time));
export default {
async asyncData() {
// fake data
await delay(500);
return {
houses: [...]
}
}
}
</script>
components/index.vue
<template>
<pre>{{ houses }}</pre>
</template>
<script>
export default {
props: {
houses: {
required: true,
type: Array
}
}
}
</script>