VUE 3 Years countdown - vue.js

I need to create a countdown for the expiration date in the VUE 3 application. I need the format of YYYY/MM/DD. my search results were using Moment.js.
After installation using:
npm i moment
I could not figure out the right way to use it.
My Code :
<template>
<p> Remaining time: {{moment(moment(30-9-2022) - moment(new Date())}}
</p>
</template>
<script>
import moment from "moment";
export default {
methods:{
moment,
}
}
</script>

const { createApp, reactive, onMounted } = Vue
const app = createApp({
data() {
const remaining = reactive({})
onMounted(() => {
setInterval(() => {
setTime()
}, 1000)
const eventTime = moment('2023.05.04 10:03:00', 'YYYY.MM.DD HH:mm:ss')
function setTime() {
let currenTime = moment()
remaining.days = moment.duration(eventTime.diff(currenTime)).asDays()
remaining.hours = moment.duration(eventTime.diff(currenTime)).get('hours')
remaining.minutes = moment.duration(eventTime.diff(currenTime)).get('minutes')
remaining.seconds = moment.duration(eventTime.diff(currenTime)).get('seconds')
}
})
return {
remaining
}
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#3/dist/vue.global.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
<div id="app">
<div v-if="Object.keys(remaining).length > 0">
{{ remaining.days.toFixed(0) }} :
{{ remaining.hours.toFixed(0) }} :
{{ remaining.minutes.toFixed(0) }} :
{{ remaining.seconds.toFixed(0) }}
</div>
</div>

I could not find the right usage for this particular problem through my research, so I thought it would be useful to publish it.
The Right Syntax :
<template>
<p>
{{ getRmainTime() }}
</p>
</template>
<script>
import moment from "moment";
export default {
methods:{
getRmainTimeToExp() {
return `${moment
.duration(moment(this.user.exp_time) - moment(new
Date())).years()} years,
${moment.duration(moment(this.user.exp_time) -
moment(new Date())).months()} months,
${moment.duration(moment(this.user.exp_time) -
moment(new Date())).days()} days`;
},
}
}
</script>
```

Related

cannot remove items from cart in vue 3 with pinia

so in the console log it is saying it is being removed from the cart but i can still see the items in the cart... how can i remove them from the cart? im using pinia for state management and the cart is in the state. why it is not working for me?
the code:
shop.vue
<template>
<div class="shop">
Cart Items: <cart-badge :count="cartLength">{{ count }}</cart-badge>
<h1>shop</h1>
<div class="products" v-for="item in Products" :key="item.id">
{{ item.name }} {{ item.price }}$
<button #click="storeCounter.addToCart(item)">Add to Cart</button>
</div>
</div>
</template>
<script setup>
import { useCounterStore } from "../stores/counter";
import Products from "../db.json";
import CartBadge from "../components/CartBadge.vue";
import { computed } from "vue";
const storeCounter = useCounterStore();
const cartLength = computed(() => {
return storeCounter.cart.length;
});
</script>
store.js(pinia)
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", {
state: () => ({
cart: [],
}),
actions: {
addToCart(id) {
this.cart.push(id);
console.log("test passed!");
},
removeFromCart(id) {
this.cart.splice(id);
console.log("removed from cart!");
},
},
});
cart.vue
<template>
<div class="cart">
<h1>cart</h1>
<div class="cartitems" v-for="item in storeCounter.cart" :key="item.id">{{ item.name }} {{ item.price }}$
<button #click="storeCounter.removeFromCart(item.id)">X</button>
</div>
</div>
</template>
<script setup>
import { useCounterStore } from '../stores/counter';
const storeCounter = useCounterStore()
</script>
Splice uses the index of the item to delete it.
Do this instead;
removeFromCart(id) {
let cartItemIndex = this.cart.findIndex(x => x.id === id);
if (cartItemIndex >= 0) {
this.cart.splice(cartItemIndex, 1);
console.log("Removed from cart");
}
}
or if you don't want to use splice anymore (and if performance matters);
removeFromCart(id) {
this.cart = this.cart.filter(x => x.id !== id);
console.log("Removed from cart");
}
Please use more descriptive parameter names. Rename "id" to "item" in addToCart, since you're not just adding the item id, but the entire item. This can be confusing to other developers and make your code unreadable.
addToCart(item) {
this.cart.push(item)
console.log('test passed!')
}
References: splice filter

Nuxt.js date time format

I have this html:
<template>
<div>
<div class="card text-center">
<h2><strong>Title: </strong>{{story.title}}</h2>
<p><strong>Score: </strong>{{story.score}}</p>
<p><strong>Author: </strong>{{story.by}}</p>
<p><strong>Date: </strong>{{ formatDate(story.time) }}</p>
<!--<p><strong>Date: </strong>{{ dateFormat(new Date(story.time*1000),v) }}</p> -->
<NuxtLink :to="`/${story.id}`">
<p class="btn my-4">View details</p> </NuxtLink>
</div>
</div>
</template>
and this scripts:
<script setup>
const { story } = defineProps(['story'])
export default {
data () {
return {
time: 0,
}
},
methods: {
formatDate(time) {
date = new Date(time).format("YYYY/MM/DD")
return date
}
}
}
</script>
I am not getting this kinda date format: "YYYY/MM/DD".
Please help how can I do that in Nuxt.js?
AFAIK, there isn't a single function call to achieve that. I would suggest you re-writing your formatDate using Date.toLocaleString to have full control of your format.
In addition, do not use export default in your <script setup>. It's not a valid syntax.
<script setup>
// Your other setup
function formatDate(time) {
const date = new Date(time);
// Get year, month, and day part from the date
const year = date.toLocaleString("default", { year: "numeric" });
const month = date.toLocaleString("default", { month: "2-digit" });
const day = date.toLocaleString("default", { day: "2-digit" });
return `${year}/${month}/${day}`;
}
</script>
You could use dayjs (tested with Nuxt 2.15)
yarn add #nuxtjs/dayjs
export default {
// ...
modules: [
'#nuxtjs/dayjs'
],
<template>
<div>
{{ $dayjs(new Date()).format('YYYY/MM/DD') }}
</div>
</template>

How to increase the number of icons according the fetched data in vue

I send an Api request and get different number of participants (from 1 to 5). According this number, the number of icons should appeared on the modal. If there are 3 participants, there should be 3 user icons in the modal.
I know how to do it in React, but I started to study Vue3 and have no idea.
<template>
<p >
Participants:
<span >
<UserIcon />
</span>
</p>
</template>
<script lang="ts">
import { defineComponent } from '#vue/runtime-core';
import {HeartIcon, UserIcon, XIcon } from '#heroicons/vue/solid'
export default defineComponent({
name: 'NewActivityModal',
components: {HeartIcon, UserIcon, XIcon},
randomActivity: {
type: Object,
required: true,
}
},
})
</script>
In React I would write in this way. But how to rewrite it in Vue? To be clear, where to put?
const participants = [
<span >
{userIcon}
</span>,
];
randomActivity.map((item) => {
for (let i = 1; i < item.participants; i++) {
participants.push(
<span className="inline-block" key={`personIcon${i}`}>
{userIcon}
</span>
);
}
return participants;
});
First you create a computed property, which returns the number of participants (the counter x for your loop).
Then you use the computed property to run the loop x times.
Is this you want to achieve?
<template>
<p >
Participants:
<span
v-for="counter in numberOfParticipants"
:key="counter"
>
<UserIcon />
</span>
</p>
</template>
<script lang="ts">
import { defineComponent } from '#vue/runtime-core';
import {HeartIcon, UserIcon, XIcon } from '#heroicons/vue/solid'
export default defineComponent({
name: 'NewActivityModal',
components: {HeartIcon, UserIcon, XIcon},
props: {
randomActivity: {
type: Object,
required: true,
}
}
},
computed: {
numberOfParticipants () {
return randomActivity
.map(item => {
return item.participants
})
.flat()
.length
}
}
})
</script>
Thank to #tho-masn I could understand. And rewrite computed properties
numberOfParticipants () {
let participants = 1
for(let i=1; i < this.randomActivity.participants; i++) {
participants += 1;
}
return participants
}

(Vue) I have problems reusing references from a composable function

I hope it is okay that I included my full code. Otherwise it would be difficult to understand my question.
I have made a composable function for my Vue application, which purpose is to fetch a collection of documents from a database.
The composable looks like this:
import { ref, watchEffect } from 'vue'
import { projectFirestore } from '../firebase/config'
const getCollection = (collection, query) => {
const documents = ref(null)
const error = ref(null)
let collectionRef = projectFirestore.collection(collection)
.orderBy('createdAt')
if (query) {
collectionRef = collectionRef.where(...query)
}
const unsub = collectionRef.onSnapshot(snap => {
let results = []
snap.docs.forEach(doc => {
doc.data().createdAt && results.push({ ...doc.data(), id: doc.id })
})
documents.value = results
error.value = null
}, (err) => {
console.log(err.message)
document.value = null
error.value = 'could not fetch data'
})
watchEffect((onInvalidate) =>{
onInvalidate(() => unsub());
});
return {
documents,
error
}
}
export default getCollection
Then I have a component where I store the data from the database
<template>
<div v-for="playlist in playlists" :key="playlist.id">
<div class="single">
<div class="thumbnail">
<img :src="playlist.coverUrl">
</div>
<div class="info">
<h3>{‌{ playlist.title }}</h3>
<p>created by {‌{ playlist.userName }}</p>
</div>
<div class="song-number">
<p>{‌{ playlist.songs.length }} songs</p>
</div>
</div>
</div>
</template>
<script>
export default {
// receiving props
props: ['playlists'],
}
</script>
And finally, I output the data inside the main Home component, where I use the documents and error reference from the composable file.
<template>
<div class="home">
<div v-if="error" class="error">Could not fetch the data</div>
<div v-if="documents">
<ListView :playlists="documents" />
</div>
</div>
</template>
<script>
import ListView from '../components/ListView.vue'
import getCollection from '../composables/getCollection'
export default {
name: 'Home',
components: { ListView },
setup() {
const { error, documents } = getCollection('playlists')
return { error, documents }
}
}
</script>
That is all well and good.
But now I wish to add data from a second collection called "books", and the idea is to use the same composable to fetch the data from that collection as well,
but the problem is that inside the Home component, I cannot use the references twice.
I cannot write:
<template>
<div class="home">
<div v-if="error" class="error">Could not fetch the data</div>
<div v-if="documents">
<ListView :playlists="documents" />
<ListView2 :books="documents" />
</div>
</div>
</template>
export default {
name: 'Home',
components: { ListView, ListView2 },
setup() {
const { error, documents } = getCollection('playlists')
const { error, documents } = getCollection('books')
return { error, documents }
}
}
This will give me an error because I reference documents and error twice.
So what I tried was to nest these inside the components themselves
Example:
<template>
<div v-for="playlist in playlists" :key="playlist.id">
<div class="single">
<div class="thumbnail">
<img :src="playlist.coverUrl">
</div>
<div class="title">
{{ playlist.title }}
</div>
<div class="description">
{{ playlist.description }}
</div>
<div>
<router-link :to="{ name: 'PlaylistDetails', params: { id: playlist.id }}">Edit</router-link>
</div>
</div>
</div>
</template>
<script>
import getCollection from '../composables/getCollection'
export default {
setup() {
const { documents, error } = getCollection('playlists')
return {
documents,
error
}
}
}
</script>
This does not work either.
I will just get a 404 error if I try to view this component.
So what is the correct way of writing this?
Try out to rename the destructed fields like :
const { error : playlistsError, documents : playlists } = getCollection('playlists')
const { error : booksError, documents : books } = getCollection('books')
return { playlistsError, playlists , booksError , books }

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