How to use Axios with Vue 3 Composition API - vue.js

I am attempting to build a Pokemon filtered search app with Vue 3 and Composition API based on the following tutorial: https://www.youtube.com/watch?v=QJhqr7jqxVo. (GitHub: https://github.com/ErikCH/PokemonVue)
The fetch method used in the search component includes a reduce() function to handle urlIdLookup based on a specific id assigned to each Pokemon in the API response:
const state = reactive({
pokemons: [],
filteredPokemon: computed(()=> updatePokemon()),
text: "",
urlIdLookup: {}
});
fetch("https://pokeapi.co/api/v2/pokemon?offset=0")
.then((res) => res.json())
.then((data) => {
console.log(data);
state.pokemons = data.results;
state.urlIdLookup = data.results.reduce((acc, cur, idx)=>
acc = {...acc, [cur.name]:idx+1 }
,{})
console.log('url',state.urlIdLookup+1)
});
urlIdLookup is then passed into the route used to display selected Pokemon info:
<div
class="ml-4 text-2xl text-blue-400"
v-for="(pokemon, idx) in filteredPokemon"
:key="idx"
>
<router-link :to="`/about/${urlIdLookup[pokemon.name]}`">
{{ pokemon.name }}
</router-link>
</div>
Instead of using the above fetch setup, I wish to use Axios to handle the request and response from the Pokemon API. After installing Axios in the project and importing it into the component, I added a new fetchPokemon method:
const fetchPokemon = () => {
axios.get('https://pokeapi.co/api/v2/pokemon?offset=0')
.then(response => {
state.pokemons = response.data
})
}
onMounted(() => {
fetchPokemon()
})
While using Axios in this new fetch method, I want to handle urlIdLookup similar to the previous fetch setup, but without using the reduce() method and de-structured accumulator, if possible. How can I go about using Axios to retrieve the urlId of each Pokemon, then pass that urlId into the "about" route in the template?
Here is the full component:
<template>
<div class="w-full flex justify-center">
<input placeholder="Enter Pokemon here" type="text"
class="mt-10 p-2 border-blue-500 border-2" v-model="text" />
</div>
<div class="mt-10 p-4 flex flex-wrap justify-center">
<div
class="ml-4 text-2xl text-blue-400"
v-for="(pokemon, idx) in filteredPokemon"
:key="idx"
>
<router-link :to="`/about/${urlIdLookup[pokemon.name]}`">
{{ pokemon.name }}
</router-link>
</div>
</div>
</template>
<script>
import axios from 'axios';
import { reactive, toRefs, computed, onMounted } from "vue";
export default {
setup() {
const state = reactive({
pokemons: [],
filteredPokemon: computed(()=> updatePokemon()),
text: "",
urlIdLookup: {}
});
const fetchPokemon = () => {
axios.get('https://pokeapi.co/api/v2/pokemon?offset=0')
.then(response => {
state.pokemons = response.data
})
}
onMounted(() => {
fetchPokemon()
})
// fetch("https://pokeapi.co/api/v2/pokemon?offset=0")
// .then((res) => res.json())
// .then((data) => {
// console.log(data);
// state.pokemons = data.results;
// state.urlIdLookup = data.results.reduce((acc, cur, idx)=>
// acc = {...acc, [cur.name]:idx+1 }
// ,{})
// console.log('url',state.urlIdLookup+1)
// });
function updatePokemon(){
if(!state.text){
return []
}
return state.pokemons.filter((pokemon)=>
pokemon.name.includes(state.text)
)
}
return { ...toRefs(state), fetchPokemon, updatePokemon };
}
};
</script>

If I understood you correctly take a look at following snippet:
const { reactive, toRefs, computed, onMounted } = Vue
const { axioss } = axios
const app = Vue.createApp({
setup() {
const state = reactive({
pokemons: [],
filteredPokemon: computed(() => updatePokemon()),
text: "",
urlIdLookup: {},
});
const fetchPokemon = () => {
axios
.get("https://pokeapi.co/api/v2/pokemon?offset=0")
.then((response) => {
state.pokemons = response.data.results; // 👈 get just results
});
};
fetchPokemon();
// 👇 function to get index
const getPokemonId = (item) => {
return state.pokemons.findIndex((p) => p.name === item);
};
function updatePokemon() {
if (!state.text) {
return [];
}
return state.pokemons.filter((pokemon) =>
pokemon.name.includes(state.text)
);
}
// 👇 return new function
return { ...toRefs(state), fetchPokemon, updatePokemon, getPokemonId };
},
})
app.mount('#demo')
<script src="https://unpkg.com/vue#3.2.29/dist/vue.global.prod.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.26.1/axios.min.js" integrity="sha512-bPh3uwgU5qEMipS/VOmRqynnMXGGSRv+72H/N260MQeXZIK4PG48401Bsby9Nq5P5fz7hy5UGNmC/W1Z51h2GQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div id="demo">
<div class="w-full flex justify-center">
<input
placeholder="Enter Pokemon here"
type="text"
class="mt-10 p-2 border-blue-500 border-2"
v-model="text"
/>
</div>
<div class="mt-10 p-4 flex flex-wrap justify-center">
<div
class="ml-4 text-2xl text-blue-400"
v-for="(pokemon, i) in filteredPokemon"
:key="i"
>
<!-- // 👇 call function to get index -->
<router-link :to="`/about/${getPokemonId(pokemon.name)}`">
{{ pokemon.name }} - id {{ getPokemonId(pokemon.name) }}
</router-link>
</div>
</div>
</div>

it seem id is not necessary, why not use name be id. if you want use interger
be must, you can foreach results set index be id to each item, then
<router-link :to="`/about/${pokemon.id}`">
{{ pokemon.name }}
</router-link>

Related

Vue blog change rendered post when clicked on a link from "latest posts" sidebar

I've got a problem with my hobby project, I created a BlogPost.vue in which I render the previously clicked post from a Blog.vue page.
In this BlogPost, I render the clicked post's title, content etc, and on the sidebar, I show a small area in which I render 3 of the latest posts from this same category - the posts from Blog.vue.
When I click on the sidebar's links, any of the 3, the browser does change the slug, and the page does re-render itself, except it re-renders the same post that was clicked. If I refresh the page in the browser (or Ctrl+R or F5 etc), then it does render the correct content clicked from this sidebar.
I have no clue why it does that, I can only suppose that I should be watching the route change then somehow refresh what it renders, but no idea as to how.
Blog.vue, this works great, renders the single post clicked
<script setup lang="ts">
import axios from "axios";
import { ref } from "vue";
import { onMounted } from "vue";
import moment from "moment";
const postsUrl = "http://localhost/wordpress/wp-json/wp/v2/posts";
const posts = ref([] as any);
const isLoading = ref(false);
const errorCaught = ref(false);
var queryOptions = {
_embed: true,
};
const getPosts = () => {
isLoading.value = true;
axios
.get(postsUrl, { params: queryOptions })
.then((response) => {
posts.value = response.data;
console.log(posts.value);
isLoading.value = false;
})
.then(() => {
console.log(isLoading.value);
})
.catch((error) => {
if (error) {
isLoading.value = false;
errorCaught.value = true;
}
});
};
onMounted(async () => {
getPosts();
});
</script>
<template>
<transition name="fadeLoading">
<div v-if="isLoading" class="posts-loading">
<div class="circle"></div>
</div>
</transition>
<transition name="fadeLoading">
<div class="errorCaught" v-if="errorCaught">
There was an error loading news
</div>
</transition>
<div class="blog-container">
<div class="wrapper">
<transition-group name="fadeBlog">
<ul v-if="!isLoading" class="blog-posts-ul" v-for="post in posts">
<div class="posts-card">
<a
><router-link
:to="/blog/ + post.slug"
key="post.id"
class="posts-permalink"
>
</router-link
></a>
<img
v-if="post.featured_media != 0"
class="posts-featuredimage"
:src="post._embedded['wp:featuredmedia'][0].source_url"
:alt="post.title.rendered"
/>
<img v-else src="#/assets/logos/favicon-big.png" />
<div class="posts-date">
<p>
{{ moment(post.date).fromNow() + " " + "ago" }}
</p>
</div>
<div class="posts-text">
<h1 class="posts-title">{{ post.title.rendered }}</h1>
<p v-html="post.excerpt.rendered" class="posts-excerpt"></p>
</div>
</div>
</ul>
</transition-group>
</div>
</div>
</template>
BlogPost.vue, renders the previously clicked one, but does not show the sidebar's clicked content
<script setup lang="ts">
import { ref, watch, onMounted } from "vue";
import { useRoute } from "vue-router";
import axios from "axios";
import moment from "moment";
const route = useRoute();
const postsUrl = "http://localhost/wordpress/wp-json/wp/v2/posts";
const queryOptions = {
slug: route.params.blogSlug,
_embed: true,
};
const post = ref([] as any);
const isLoading = ref(false);
const latestPostsAPI = "http://localhost/wordpress/wp-json/wp/v2/posts";
const latestPosts = ref([] as any);
const errorCaughtLatest = ref(false);
var queryOptionsLatest = {
_embed: true,
per_page:3,
};
const getLatest = () => {
axios
.get(latestPostsAPI, { params: queryOptionsLatest })
.then((response) => {
latestPosts.value = response.data;
console.log(latestPosts.value);
})
.catch((error) => {
if (error) {
errorCaughtLatest.value = true;
}
});
};
const getPost = () => {
isLoading.value = true;
axios
.get(postsUrl, { params: queryOptions })
.then((response) => {
post.value = response.data;
console.log("Pages retrieved!");
})
.catch((error) => {
console.log(error);
})
.then(() => {
isLoading.value = false;
});
};
getLatest();
getPost();
</script>
<template>
<div v-if="!isLoading" class="post-wrapper">
<div class="wrapper">
<div class="post-title">{{ post[0].title.rendered }}</div>
<div class="post-date">
{{ moment(post[0].date).format("MMMM Do YYYY, h:mm, dddd") }}
</div>
<!-- THIS INCLUDES HTML TAGS -->
<div class="post-content" v-html="post[0].content.rendered"></div>
</div>
</div>
<div class="side-container">
<div class="side-wrapper">
<ul v-if="!isLoading" class="blog-posts-ul" v-for="latest in latestPosts">
<div class="posts-card">
<a
><router-link
:to="/blog/ + latest.slug"
key="latest.id"
class="posts-permalink"
>
</router-link
></a>
<img
v-if="latest.featured_media != 0"
class="posts-featuredimage"
:src="latest._embedded['wp:featuredmedia'][0].source_url"
:alt="latest.title.rendered"
/>
<img v-else src="#/assets/logos/favicon-big.png" />
<div class="posts-text">
<div class="posts-title">{{ latest.title.rendered }}</div>
<div class="posts-date">
<p>{{ moment(latest.date).fromNow() + " " + "ago" }}</p>
</div>
<div class="posts-author">
{{ latest._embedded.author[0].name }}
</div>
</div>
</div>
</ul>
</div>
</div>
</template>
Thanks for your time
In my original post as you can see, I'm using axios to get my post with the params queryOptions:
I declare my queryOptions with a const:
const queryOptions = {
slug: route.params.blogSlug,
_embed: true,
};
THEN, I call the getPost method, which uses this constant:
const getPost = () => {
isLoading.value = true;
axios
.get(postsUrl, { params: queryOptions } })
.then((response) => {
post.value = response.data;
console.log("getPost!");
})
.catch((error) => {
console.log(error);
})
.then(() => {
isLoading.value = false;
});
};
And the problem is right there: that queryOptions, IS a constant, gets defined at the onMounted lifecycle, or even before I'm not 100% sure, but it does NOT get re-defined WHEN I click on any link on this BlogPost.vue component.
SO, the solving is the following:
Change the getPost method so it does NOT use a pre-defined constant, rather just simply type in the params, like the following:
const getPost = () => {
isLoading.value = true;
axios
.get(postsUrl, { params: { slug: route.params.blogSlug, _embed: true } })
.then((response) => {
post.value = response.data;
console.log("getPost!");
})
.catch((error) => {
console.log(error);
})
.then(() => {
isLoading.value = false;
});
};
If I do it this way, every time the getPost method runs, which it WILL since I'm watching the blogSlug change with
watch(() => route.params.blogSlug, getPost);
the getPost function will always call the latest route.params.blogSlug, which gets changed before the watcher runs the getPost as I click on the sidebar link.

Prop being passed but not updating the component

So I have a function called updateAll(data.items) which should take the array and randomly assign a new name, it appears the data.items is updating when I view in vue dev tools and is being passed to the component but not actually updating the browser
I've also added in my buildData.js this is being used for the same app but in react so was trying to keep it as general as I could
//HomeView.vue
<script setup>
import { reactive, watch } from "vue";
import Hero from '#/components/Hero.vue'
import FunctionButton from '#/components/FunctionButton.vue'
import ItemsContainer from '#/components/ItemContainer.vue';
import * as dataFunc from '#/data/buildData'
const data = reactive({
items: JSON.parse(localStorage.getItem('items')) || []
})
console.log(data.items)
watch(() => data.items,(newValue) => {
localStorage.setItem('items', JSON.stringify(newValue))
}, {deep: true});
</script>
<template>
<div class='w-2/3 mx-auto text-center'>
<Hero HeroText="The one stop shop to store recipes!" />
<main>
<h1 class="text-5xl font-extrabold dark:text-white">Recipe Manager</h1>
<div class='functions gap-4 grid grid-cols-2 md:grid-cols-3 mt-8 mx-auto w-fit'>
<FunctionButton name="Add new recipe" #click="() => data.items = [...data.items,...dataFunc.buildData(1)]"/>
<FunctionButton name="Add 100 recipes" #click="() => data.items = [...data.items,...dataFunc.buildData(100)]"/>
<FunctionButton name="Add 1000 recipes" #click="() => data.items = [...data.items,...dataFunc.buildData(1000)]"/>
<FunctionButton name="Delete all recipes" #click="() => data.items = dataFunc.deleteAll()"/>
<FunctionButton name="Edit all recipes" #click="() => data.items = dataFunc.updateAll(data.items)"/>
</div>
<ItemsContainer :items="data.items"/>
</main>
</div>
</template>
//ItemContainer.vue
<script setup>
import Item from '#/components/Item.vue';
const props = defineProps({
items: {type: Array, default: () => []},
})
</script>
<template>
<div
class='items-container w-full md:w-max bg-gray-800 dark:bg-gray-800 text-white dark:text-black mx-auto mt-12 p-4 space-y-4'>
<Item
v-for="item in items"
v-if="items.length > 0"
:key="item.id"
:meal="item"
/>
<p v-else class='text-white'>No items to display</p>
</div>
</template>
I tried to make this as generic as possible as trying to make a similar app on Vue and React so they could share this
import { v4 as uuidv4 } from 'uuid';
function create(){
const data = {
id: uuidv4(),
category: category[random(category.length)],
name: meals[random(meals.length)],
prepTime: randomTime(),
cookTime: randomTime(),
isFav: false
}
return data
}
function random(max){
return Math.round(Math.random() * 1000) % max;
}
const meals = [
"Roast Chicken", "Omelette", "Steak Pie", "Beef Stir Fry", "Fish And Chips", "Tomato And Prawn Risotto", "Pepperoni Pizza", "Cheesy Nachos", "Fajitas", "Baked Potato",
"Full English Breakfast", "Pancakes", "Chocolate Brownies", "Meatballs With Red Pepper Sauce", "Chicken Cesar Salad", "Beef Burger", "Chips","Macaroni Cheese", "Fillet Steak", "Chicken Wings",
"BBQ Ribs", "Tomato Soup", "Prawn Dim Sum", "Pork Gyozas", "Tomato Bruschetta", "Spring Rolls", "Beef Jerky", "Lasagne", "Spagetti Carbonara", "Salmon And Potatoes"
]
const category = [
"Breakfast", "Lunch", "Dinner"
]
function randomTime(){
return Math.round(Math.random() * 30)
}
function buildData(count){
const data = new Array(count);
for(let i = 0; i<count; i++){
data[i] = create();
}
return data;
}
function updateAll(currentArray){
return currentArray.map((item) => {
return{...item, name: meals[random(meals.length)], }
})
}
function updateOne(item){
return{...item, name: meals[random(meals.length)], }
}
function favouriteAll(currentArray = []){
return currentArray.map((item) => {
return {...item, isFav: true}
})
}
function deleteAll(){
const data = []
return data;
}
export {buildData, deleteAll, favouriteAll, updateAll, updateOne};
Check the console for errors.
If I take the functionality from HomeView.vue, your ItemContainer.vue and an Item.vue (which you didn't provided), then it works.
Check the working Vue SFC Playground
The code:
App.vue
<script setup>
import { ref, watch, reactive } from 'vue'
import ItemsContainer from './ItemContainer.vue';
const data = reactive({
items: []
})
console.log(data.items)
watch(() => data.items, (newValue) => {
alert(JSON.stringify(newValue))
}, {deep: true});
const addItem = () => {
data.items.push({ id: data.items.length, name: 'New Item'})
}
</script>
<template>
<ItemsContainer :items="data.items"/><br />
<button type="button" #click="addItem()">Add</button>
</template>
ItemContainer.vue is the same.
Item.vue
<script setup>
const props = defineProps({
meal: {
type: Object,
default: {undefined}
}
})
</script>
<template>
<div>
{{JSON.stringify(meal)}}<br />
</div>
</template>
UPDATE
I've updated the playground with your functions and it still does work well. The problem is somewhere else.

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

Show login user in vue header

Show login user in vue header
I'm not using pinia, vuex, etc. When I try to log in, I saved the loginId in localstorage, but I want to retrieve it from header.vue and display the logged in user. Is there any way?
The code is lacking, but please help
If you can't give me the code on how to display the user, I'd appreciate it if you could provide a reference.
I prefer the syntax of vue3
login.vue
<template class="login">
<div class="login_box">
<h3>welcome!</h3>
<div class="login_form">
<form #submit.prevent="submit()">
<div class="login_id">
<input
v-model="state.login.loginId"
type="email"
placeholder="E-mail" />
<span class="login_icon">
<img src="../../public/images/people_icon.svg" />
</span>
</div>
<div class="login_pass">
<input
v-model="state.login.loginPw"
type="password"
placeholder="password" />
<span class="login_icon">
<img src="../../public/images/lock.svg" />
</span>
</div>
<p class="login_go">
Not id.
<router-link to="/signup">
<span>signup</span>
</router-link>
</p>
<button
class="login_btn">
Login
</button>
</form>
</div>
</div>
</template>
<script>
import axios from 'axios'
import { useRouter } from 'vue-router'
import { reactive } from 'vue'
export default {
setup() {
const router = useRouter()
const state = reactive({
login: {
loginId: '',
loginPw: '',
},
})
const submit = async () => {
const args = new FormData()
args.append('username', state.login.loginId)
args.append('password', state.login.loginPw)
console.log(state.login)
try {
await axios.post('http://127.0.0.1:8000/login/token', args, {
header: { 'Content-Type': 'application/json'}, withCredentials:true
})
.then((res) => {
console.log(res.data)
localStorage.setItem('loginId', state.login.loginId)
localStorage.setItem('access_token', `Bearer ${res.data.access_token}`)
document.cookie = `access_token=Bearer ${res.data.access_token}`
})
alert('welcome')
router.push({
name: 'home',
params: {
args
}
})
} catch (error){
alert('Login Faild')
console.error(error)
}
}
return { state, submit }
},
}
</script>
header.vue
<template>
<div class="header">
<div class="header_wrap">
<img
class="logo"
style="cursor:pointer"
#click="dashboard()" />
<ul class="gnb">
<li>
<router-link to="/service_center/notice">
Service center
</router-link>
</li>
</ul>
<ul class="tnb" v-if=loggin>
<li>
{{ $route.params.loginId}}
</li>
<li>
logout
</li>
</ul>
<ul class="tnb" v-else>
<li>
<router-link to="/login">
login
</router-link>
</li>
<li>
<router-link to="/signup">
signup
</router-link>
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const dashboard = () => {
router.push({
path:'/home',
})
}
</script>
Some things that may help...
Decouple the authentication.
moving the authentication in a separate file will make it more accessible between multiple components. This can act as your dedicated non-pina, not-vuex state.
Track state as reactive and make localStorage secondary
export const loginState = reactive({
loginId: "eve.holt#reqres.in",
loginPw: "",
token: localStorage.getItem('my-token') || null,
apiState: STATES.INIT,
error: null,
});
This example will set the value from local storage if available. the rest of the application can use that value to determine the value.
then update the localStorage only within this file.
export const login = () => {
loginState.apiState = STATES.PROCESSING;
loginState.error = null;
fetch(
"https://reqres.in/api/login", {
method:"POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: loginState.loginId,
password: loginState.loginPw
})
})
.then(res => res.json())
.then(({token, error}) => {
if(error) throw new Error(error)
loginState.token = token;
localStorage.setItem('my-token', token)
loginState.apiState = STATES.SUCCESS;
})
.catch(error => {
console.error(error);
loginState.error = error.message;
loginState.apiState = STATES.ERROR;
})
}
export const logout = () => {
loginState.token = null;
login.apiState = STATES.INIT;
localStorage.removeItem('my-token')
}
here only login and logout update the state, the rest of the app can use other functions.
make helpers available
export const isLoggedIn = computed(() => loginState.token !== null);
export const hasError = computed(() => loginState.error !== null);
export const isProcessing = computed(() => loginState.apiState === STATES.PROCESSING);
instead of checking the state directly, you can rely on derived state using computed.
in fact, I would not store username and password in a state, but have them passed in in the login function and the state can then be internal
here is an example
note that the example uses fetch instead of and an available api from https://reqres.in/ to make it work with fewer dependencies

Vuex store Getter loadings faster than store State

I have a nuxtJS vue2 components as follows:
<template>
<div class="product-fullpage">
<div class="product-card">
<img :src="productById(this.$route.params.id).imageURL">
<div class="product-headings">
<div class="product-info" v-animate-on-scroll>
<h1>{{ productById(this.$route.params.id).name }}</h1>
<h2>£{{ productById(this.$route.params.id).price }}</h2>
</div>
<div class="product-cart" v-animate-on-scroll>
<div class="quantity-info">
<label for="quantity">Quantity:</label>
<input v-model.number="productInfo.quantity" name="quantity" type="number" min="0" max="99"/>
</div>
<button #click="addToCart" :disabled="noQuantity">Add To Cart ></button>
</div>
</div>
</div>
</div>
</template>
I'm using a getter that does the following:
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['productById'])
},
}
</script>
Here's the getter
export const getters = {
productById: (state) => (id) => {
return state.products.find(product => product.id === id)
},
}
my state is set to pull from firebase
export const actions = {
async setProducts({commit}) {
let colRef = collection(db, 'products')
const querySnapshot = await getDocs(colRef)
querySnapshot.forEach((doc) => {
const imageDownloadURL = getDownloadURL(ref(storage, `${doc.data().imageRef}`))
.then( url => {
// console.log(url)
let article = ({
id: doc.id,
name: doc.data().name,
price: doc.data().price,
description: doc.data().description,
imageURL: url
})
commit('setProducts', article)
})
})
},
}
the mutation to set the state:
export const mutations = {
setProducts(state, article) {
let matchProduct = state.products.find(product => product.id == article.id)
if(!matchProduct) {
state.products.push(article)
}
},
}
and this is my state:
export const state = () => ({
products: [],
})
i thought that if i load everything beforehand in default.vue under 'layouts' that i can then have the store.state.products set.
<template>
<div class="container">
<!-- <nuxt /> -->
<nuxt v-if="!loading"/>
<div class="overlay" v-else>
Loading...
</div>
</div>
</template>
<script>
export default {
created() {
this.loading = true
this.$store.dispatch('setCart')
this.$store.dispatch('setProducts')
.finally(() => (this.loading=false))
},
data() {
return {
loading: false,
}
},
}
</script>
sorry if this is turning out to be a long post - but basically on initial load, I get my imageURL, name and price. but then on reload it comes out empty. i believe the getter is occurring before the store is loaded. how do i set it so that i can state.products.find(product => product.id == article.id) for my getter after state is loaded?