I have a nuxt.js project: https://github.com/AzizxonZufarov/newsnuxt2
I need to update posts from API every minute without loading the page:
https://github.com/AzizxonZufarov/newsnuxt2/blob/main/pages/index.vue
How can I do that?
Please help to end the code, I have already written some code for this functionality.
Also I have this button for Force updating. It doesn't work too. It adds posts to previous posts. It is not what I want I need to force update posts when I click it.
This is what I have so far
<template>
<div>
<button class="btn" #click="refresh">Force update</button>
<div class="grid grid-cols-4 gap-5">
<div v-for="s in stories" :key="s">
<StoryCard :story="s" />
</div>
</div>
</div>
</template>
<script>
definePageMeta({
layout: 'stories',
})
export default {
data() {
return {
err: '',
stories: [],
}
},
mounted() {
this.reNew()
},
created() {
/* setInterval(() => {
alert()
stories = []
this.reNew()
}, 60000) */
},
methods: {
refresh() {
stories = []
this.reNew()
},
async reNew() {
await $fetch(
'https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty'
).then((response) => {
const results = response.slice(0, 10)
results.forEach((id) => {
$fetch(
'https://hacker-news.firebaseio.com/v0/item/' +
id +
'.json?print=pretty'
)
.then((response) => {
this.stories.push(response)
})
.catch((err) => {
this.err = err
})
})
})
},
},
}
</script>
<style scoped>
.router-link-exact-active {
color: #12b488;
}
</style>
This is how you efficiently use Nuxt3 with the useLazyAsyncData hook and a setInterval of 60s to fetch the data periodically. On top of using async/await rather than .then.
The refreshData function is also a manual refresh of the data if you need to fetch it again.
We're using useIntervalFn, so please do not forget to install #vueuse/core.
<template>
<div>
<button class="btn" #click="refreshData">Fetch the data manually</button>
<p v-if="error">An error happened: {{ error }}</p>
<div v-else-if="stories" class="grid grid-cols-4 gap-5">
<div v-for="s in stories" :key="s.id">
<p>{{ s.id }}: {{ s.title }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { useIntervalFn } from '#vueuse/core' // VueUse helper, install it
const stories = ref(null)
const { pending, data: fetchedStories, error, refresh } = useLazyAsyncData('topstories', () => $fetch('https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty'))
useIntervalFn(() => {
console.log('refreshing the data again')
refresh() // will call the 'topstories' endpoint, just above
}, 60000) // every 60 000 milliseconds
const responseSubset = computed(() => {
return fetchedStories.value?.slice(0, 10) // optional chaining important here
})
watch(responseSubset, async (newV) => {
if (newV.length) { // not mandatory but in case responseSubset goes null again
stories.value = await Promise.all(responseSubset.value.map(async (id) => await $fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty`)))
}
})
function refreshData() { refreshNuxtData('topstories') }
</script>
basically my error is that when I run npm run build on the project it points out that it can't find my manuals list
**npm error:
npm run build
vue-tsc --noEmit && vite build
src/views/ManuaisView.vue:11:80 - error TS2304: Cannot find name 'manuais'.
11 **
Found 1 error.
Code:
<div class="flex flex-wrap -mx-4">
<div class="w-full md:w-1/2 lg:w-1/3 px-4" v-for="(manual, idx) in manuais" :key="manual.key">
<div class="h-full p-8 text-center hover:bg-white rounded-md hover:shadow-xl transition duration-200">
<div class="inline-flex h-16 w-16 mb-6 mx-auto items-center justify-center text-white bg-vermelho rounded-lg">
<i class="far fa-file-pdf fa-2x"></i>
</div>
<h3 class="mb-4 text-xl md:text-2xl leading-tight font-bold">{{manual.nome}}</h3>
<p>{{manual.descritivo}}</p>
<p>{{manual.link}}</p>
</div>
</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import { meudb } from '../db';
var post:any = [];
var items: any;
const postRef = meudb.ref('********').once('value', (snapshot) => {
const documents = snapshot.val();
snapshot.forEach(d => {
post.push({nome:d.val().nome, descritivo:d.val().descritivo, link:d.val().nivel})
});
items = documents
console.log(post);
// do something with documents
});
console.log(postRef);
export default {
data() {
return {
manuais: post,
// manuais: [],
}
},
firebase: {
manuais: {
source: meudb.ref('********'),
asObject: true
}
}
}
</script>
If you want to update the manuais value you should set it to an empty array in its definition (in the data) and update its value in the mounted lifecycle (check documentation here
Example :
export default {
data: () => {
return {
manuais: [],
}
},
mounted() {
var post: any = [];
var items: any;
const postRef = meudb.ref('********').once('value', (snapshot) => {
const documents = snapshot.val();
snapshot.forEach(d => {
post.push({
nome: d.val().nome,
descritivo: d.val().descritivo,
link: d.val().nivel
})
});
items = documents
// do something with documents
});
this.manuais = post; // <-- here updating the value
}
}
Working snippet
Here is a little snippet for you to better undestand how works v for.
In your case, if you apply this logic, your code might work
new Vue({
el: '#app',
data: () => ({
manuais: []
}),
mounted() {
// get data from dataBase here
this.manuais = [{
name: 'foo'
}, {
name: 'bar'
}]
}
})
<script src="https://unpkg.com/vue#2.x/dist/vue.js"></script>
<div id="app">
<div v-for="manual in manuals">{{ manual.name }}</div>
</div>
I want the dropdown to be opened when ul is clicked on the menu I want, in the same way, when the 2nd dropdown is clicked, the open dropdown will be closed.
<template>
<li>
<MenuLink
:link="items"
:key="items.name"
#click.stop="clickShow()"
/>
<ul class="children" v-if="hasChild" :class="open ? 'show' : 'hidden'">
<MenuItems
v-for="subItem in items.children"
:key="subItem.name"
:items="subItem"
/>
</ul>
</li>
</template>
<script>
export default {
name: 'MenuItems',
components: {MenuLink},
props: {
items: {type: Object, required: true},
level: {
type: Number,
default: 0
}
},
data() {
return {
childrenCount: 0,
active: false,
show: null,
open: false,
};
},
This is a I found that worked for me.
Also, I used the vue composition api, which I highly recommend using.
<template>
<ul v-if="showDropdownX">
<li>This is X</li>
<li>This is X</li>
<li>This is X</li>
</ul>
<button #click="handleShowX">Toggle dropdown X</button>
<ul v-if="showDropdownY">
<li>This is Y</li>
<li>This is Y</li>
<li>This is Y</li>
</ul>
<button #click="handleShowY">Toggle Dropdown Y</button>
</template>
<script>
import { ref } from "#vue/reactivity";
export default {
setup() {
const showDropdownX = ref(false);
const showDropdownY = ref(false);
const handleShowX = () => {
showDropdownX.value = !showDropdownX.value;
showDropdownY.value = false;
};
const handleShowY = () => {
showDropdownY.value = !showDropdownY.value;
showDropdownX.value = false;
};
return {
showDropdownX,
showDropdownY,
handleShowX,
handleShowY,
};
},
};
</script>
I hope this works for you, I haven't used the exact same code which you used, but I think my example is great to understand the concept of v-if
Edit:
Now the other dropdown will also disappear when you click on one of them.
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
I'm trying to conditionally render a component in VueJS after an Http request right away when the application starts. If the response is ok I would like to render component 1, otherwise component 2. I would also like to render the component onClick
App.vue
<template>
<div id="app">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="navbar-collapse" id="navbarsExample05">
<ul class="navbar-nav pl-md-5 ml-auto">
<li v-for="tab in tabs" v-bind:key="tab" v-bind:class="['nav-item nav-link', { active: currentTab === tab }]"
v-on:click="currentTab = tab">
{{ tab }}
</li>
</ul>
</div>
</nav>
<component v-bind:is="currentTabComponent" class="tab"></component>
</div>
</template>
<script>
import Comp1 from '../components/comp1'
import Comp2 from '../components/comp2'
export default {
name: 'App',
components: {
Comp1,
Comp2
},
data: function() {
return {
currentTab: 'Comp2',
tabs: ['Comp1', 'Comp2']
};
},
computed:{
currentTabComponent: function () {
function check(){
fetch('someHttpUrl')
.then(response => response.json())
.then(data => {
resolve('Comp1')
});
.catch(err => {
resolve('Comp2')
})
}
var result = check();
result.then(async function (data) {
return data
})
}
}
}
</script>
When I click on the tab, the right component is loaded. But not when the application starts.
Is there any Vue method to render asynchronous a component?
There is no need to have currentTabComponent computed method.
You can just make your HTTP call and update currentTab when it is done.
Something like this:
mounted() {
fetch('someHttpUrl')
.then(response => response.json())
.then(data => {
this.currentTab = 'Comp1'
});
.catch(err => {
this.currentTab = 'Comp2'
})
}
I also removed the method named check as it seemed redundant.