Vue 3 wait for object to be fetched - javascript

I am new to Vuejs and would need some help, please. I am calling a component in order to create a 'member'. While calling this component and passing the member data, the component raises an error related to one of the functions inside. The issue seems that the member data is not being initially fulfilled considering that the member data is being fetched via an API. I have tried converting the function which causes the error to Asynchronous but that does not seem to work either. Below please find some code snippets for my issue.
Home.vue
<Member :member="memb" />
data() {
return {
memb: {},
};
},
//Method fetching the member from api
async fetchMember(memberId) {
const res = await axios.get(`http://localhost:3000/members/${memberId}`, { apiHeaders });
return res.data;
},
//Method which fetched the member
async created() {
this.memb = await this.fetchMember(1);
},
Member.vue
<template>
<div class="m-auto">
<p>Name: {{ member.name }}</p>
<p>Surname: {{ member.surname }}</p>
<p>Date of Birth: {{ formatDob(member.dob) }}</p>
<p>Age: {{ calculateAge(member.dob) }}</p>
<p>Relationship: {{ member.relationship }}</p>
<button class="bg-blue-500 py-1 px-4 rounded-md text-gray-100 text-sm mx-1 mt-3 font-bold" #click="$emit('get-member-id', member.id)">Update</button>
<button class="bg-red-500 py-1 px-4 rounded-md text-gray-100 text-sm mx-1 mt-3 font-bold" #click="$emit('delete-member', member.id)">Delete</button>
</div>
</template>
<script>
export default {
name: 'Member',
props: {
member: Object,
},
methods: {
// Converting the date of birth format
formatDob(dob) {
return dob.split('/').reverse().join('/');
},
// Calculating the date of birth by comparing today's date with the member's date of birth
calculateAge(dob) {
const birthdate = new Date(dob);
const todayDate = new Date();
const difference = todayDate - birthdate; // This is the difference in milliseconds
return Math.floor(difference / 31557600000);
},
},
};
</script>
The issue that I am getting is related to calling the formatDOB from the Member component. In fact the issue says: Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'split')
Thanks in advance.

Will be cool if we can implement a progress indicator meant for informing the users about loading activity of data
Once the member data loads, you can start showing the member component and hide the progress indicator.
Pseudocode would look like something below.
<Member v-if="isLoaded" :member="memb" />
<ProgressBar v-if="!isLoaded" />
<script>
export default {
data: (() => {
return {
isLoaded: false,
memb: null
}
})
async mounted() {
this.isLoaded = false;
this.memb = await this.fetchMember(1);
this.isLoaded = true;
}
}
</script>

Related

Deleting single value from Pinia reactive data store

I am creating a multistep register form in which I provide an avatar upload. Because it is a multistep form, I want to store the data in a Pinia store until the form finally gets submitted. Everything works fine so far. But I want to be able to delete the value that contains the Blob URL for the avatar, so the user can choose a different image. What I am trying to do is this userRegisterStore.cardOwner.avatar = '' cause the initial state of that value is just an empty string. But I get this error message:
runtime-core.esm-bundler.js:218 Uncaught TypeError: 'set' on proxy: trap returned falsish for property 'avatar'
I also use cropperjs and vue-cropperjs. But I think that's irrelevant in this case.
I Googled all day and found nothing. So, I hope someone here can help.
[EDIT]
I created a codesandbox.io I hope it works. The first file you should see is RegisterFormFive.vue. To view it, you need to go to this link or use the integrated preview in codesandbox: https://n9dfv3-5173.preview.csb.app/register. Then upload an image, crop it (orange button beneath the image), and then try to delete it (red button)
Here's my code:
// RegisterDataStore.js
export const useRegisterDataStore = defineStore('RegisterDataStore', {
state: () => ({
imgReady: false,
cardOwner: reactive({
firstName: '',
lastName: '',
email: '',
password: '',
agbAccepted: false,
dsgvoAccepted: false,
title: '',
companyName: '',
companyPublic: false,
position: '',
positionPublic: false,
avatar: '',
addresses: [],
contacts: [],
links: [],
}),
}),
// Cropper part
<Cropper
v-if="registerDataStore.cardOwner.avatar && !registerDataStore.imgReady"
class="mx-auto max-h-[350px] max-w-[350px] overflow-hidden rounded-lg border-2 border-skin-primary bg-skin-primary"
ref="cropper"
alt="User avatar"
drag-mode="move"
:src="registerDataStore.cardOwner.avatar"
:aspect-ratio="1 / 1"
:crop-box-movable="false"
:crop-box-resizable="false"
:auto-crop-area="0.6"
:guides="false"
:movable="true"
:scalable="true"
:zoomable="true"
:zoo-on-touch="true"
:max-canvas-width="350"
:max-canvas-height="350"
:zoom-on-wheel="true"
:rotate-on-drag="false"
:rotatable="false"
:background="false"
:modal="true"
:initial-aspect-ration="1 / 1"
:view-mode="1"
></Cropper>
// Conponent script
<script setup>
import HeaderNav from '#/components/HeaderNav.vue'
import HeaderTitle from '#/components/HeaderTitle.vue'
import { useRegisterDataStore } from '#/stores/RegisterDataStore'
import Cropper from 'vue-cropperjs'
import 'cropperjs/dist/cropper.css'
import { ref } from 'vue'
import { useObjectUrl } from '#vueuse/core'
name: 'RegisterFormFive'
const registerDataStore = useRegisterDataStore()
const avatarInput = ref(null)
const cropper = ref(null)
const fileChanged = (event) => {
const file = event.target.files[0] || e.dataTrtansfer.files[0]
const reader = new FileReader()
reader.onload = (e) => {
registerDataStore.cardOwner.avatar = e.target.result
}
reader.readAsDataURL(file)
}
const deleteAvatar = (event) => {
registerDataStore.cardOwner.avatar = null
registerDataStore.imgReady = false
}
</script>
// The button that tiggers the storage
<div class="mt-4 flex justify-center">
<button
v-if="!registerDataStore.imgReady"
#click.prevent="
cropper.getCroppedCanvas().toBlob((blob) => {
registerDataStore.cardOwner.avatar = useObjectUrl(blob)
registerDataStore.imgReady = true
})
"
type="button"
class="hover:bg-skin-primary-dark inline-flex items-center rounded-md border border-transparent bg-skin-primary px-4 py-2 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-skin-primary focus:ring-offset-2"
>
// The file input field
<input
type="file"
ref="avatarInput"
accept=".jpg,.png"
#change="fileChanged"
:style="{ display: 'none' }"
/>
// The button that should "delete" the value
<button
v-if="registerDataStore.imgReady"
#click.prevent="deleteAvatar"
type="button"
class="hover:bg-skin-primary-dark inline-flex items-center rounded-md border border-transparent bg-red-700 px-4 py-2 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-skin-primary focus:ring-offset-2"
>
<IconWarning
class="mr-2 h-5 w-5 fill-current text-skin-primary"
aria-hidden="true"
/>
Bild löschen
</button>
Nested reactive isn't needed in state, Pinia state is already reactive. #click.prevent handler doesn't need to be created in a template, it doesn't affect how it works but makes debugging harder.
VueUse useObjectUrl composable is the problem. Due to how Vue reactive API works, refs are unwrapped inside reactive object. Since useObjectUrl returns readonly ref, it makes cardOwner.avatar property readonly and prevents from reassigning a value. Changing it would require the whole object to be reassigned:
registerDataStore.cardOwner = { ...registerDataStore.cardOwner, avatar: ... }
The actual problem is that useObjectUrl is misused. Since blob value doesn't change in the scope of then function, it can't benefit from being reactive. The composable should be replaced with the actual thing that it does:
registerDataStore.cardOwner.avatar = URL.createObjectURL(newObject)

Snipcart Crawling product failed in a nextjs app

I'm using snipcart in a nextjs with strapi app.
I receive an error when processing a payment :
A 'cart-confirmation' error occured in Snipcart. Reason:
'product-crawling-failed'
[Details] We have not been able to find item with id '59' at
'https://cmeditions.fr/products/selfless'. Please make sure the
product is correctly defined.
and :
An attempt to create an order with invalid products has been made.
Don't forget you can't add/update items attributes in the cart
dynamically with javascript as it will cause crawling issues
It works when data-item-id is replaced by a static value but not with somthing like {product.id}
Here is my component [sulg].js :
const ProductPage = ({ product }) => {
const router = useRouter()
if (router.isFallback) {
return <div>Loading product...</div>
}
return (
<div className="carousel-container m-6 grid grid-cols-1 gap-4 mt-8 items-start">
<Head>
<title>{product.title}</title>
</Head>
<div className="rounded-t-lg pt-2 pb-2 m-auto h-auto">
<EmblaCarousel {...product} />
</div>
<div className="w-full p-5 flex flex-col justify-between">
<div>
<h4 className="mt-1 font-semibold leading-tight truncate text-slate">
<span className="uppercase text-lg font-bold">{product.title}</span>
<br />
{product.author}
</h4>
<LanguageDesc {...product} />
</div>
<div className="flex flex-col md:flex-row">
<div className="mt-4 font-semibold leading-tight truncate text-slate">
<p>{product.year}</p>
<p>Format: {product.size}</p>
<p>Printing: {product.technic}</p>
<p>Paper: {product.medium}</p>
<p>{product.page}</p>
<p>{product.copies} copies</p>
<p>{product.price}€</p>
</div>
{product.status === "published" ? (
<button
className="snipcart-add-item mt-4 ml-16 self-center bg-slate border border-gray-200 d hover:shadow-lg hover:bg-brique focus:outline-none text-white font-semibold leading-none py-2 px-4 w-20 h-20 rounded-full"
data-item-id={product.id}
data-item-price={product.price}
data-item-url={router.asPath}
data-item-description={product.description}
data-item-image={getStrapiMedia(
product.image.formats.thumbnail.url
)}
data-item-name={product.title}
v-bind="customFields"
>
Add to cart
</button>
) : (
<div className="text-center mr-10 mb-1" v-else>
<div
className="p-2 bg-indigo-800 items-center text-indigo-100 leading-none lg:rounded-full flex lg:inline-flex"
role="alert"
>
<span className="flex rounded-full bg-indigo-500 uppercase px-2 py-1 text-xs font-bold mr-3">
Coming soon...
</span>
<span className="font-semibold mr-2 text-left flex-auto">
This article is not available yet.
</span>
</div>
</div>
)}
</div>
</div>
</div>
)
}
export async function getStaticProps({ params }) {
const product = await getProduct(params.slug)
return { props: { product } }
}
export async function getStaticPaths() {
const products = await getProducts()
return {
paths: products.map((_product) => {
return {
params: { slug: _product.slug },
}
}),
fallback: true,
}
}
export default ProductPage
EDIT
I tried to return a json to the validator, but I think it isn't right
/api/products.js
import { getProducts } from "../../utils/api"
// eslint-disable-next-line import/no-anonymous-default-export
export default async function (_req, res) {
return new Promise((resolve) => {
getProducts()
.then((response) => {
res.statusCode = 200
res.setHeader("Content-Type", "application/json")
res.setHeader("Cache-Control", "max-age=180000")
res.end(JSON.stringify(response))
resolve()
})
.catch((error) => {
res.json(error)
res.status(405).end()
resolve() // in case something goes wrong in the catch block (as vijay commented)
})
})
}
api.js
export function getStrapiURL(path) {
return `${
process.env.NEXT_PUBLIC_STRAPI_API_URL || "http://localhost:1337"
}${path}`
}
// Helper to make GET requests to Strapi
export async function fetchAPI(path) {
const requestUrl = getStrapiURL(path)
const response = await fetch(requestUrl)
const data = await response.json()
return data
}
export async function getCategories() {
const categories = await fetchAPI("/categories")
return categories
}
export async function getCategory(slug) {
const categories = await fetchAPI(`/categories?slug=${slug}`)
return categories?.[0]
}
export async function getProducts() {
const products = await fetchAPI("/products")
return products
}
export async function getProduct(slug) {
const products = await fetchAPI(`/products?slug=${slug}`)
return products?.[0]
}
export async function getLibrairies() {
const librairies = await fetchAPI("/librairies")
return librairies
}
export async function getLibrairie(slug) {
const librairies = await fetchAPI(`/librairies?slug=${slug}`)
return librairies?.[0]
}
It sounds like you will need to validate the orders with a JSON file that you can assign to the buy button with data-item-url.
You can read the full guide here:
https://docs.snipcart.com/v3/setup/order-validation#json-crawler.
Let me know if that helps!
It finally wasn't a matter of JSON crawler but something to see with the domain on the snipcart dashboard. I had to put the vercel.app domain first and my own domain name as a subdomain. It worked when I changed this. This is the only explaination I got

how can i do a call to axios wtihout fall in a infinite loop

i did a netflix copy with two different Axios call for series and films... Now i want add an Axios call for actors... and i did it... but i fall in an infinite loop, so after one minute the page crash... do you know why???
This is a CardFilm.vue that is repeated with a v-for in MainFilm.vue...
In my card i ve got all Film info from film's API... i want add a section for actors, so i m taking another API for actors... i had back 20 objects where i take just the element.name (actor name), with this.arrayAttori.length i take just the first 5 element of the array ACTORS of API, but after that it work i fall in an infinite loop, because my code ask infinite time the ACTORS ARRAY of API
TEMPLATE
<template>
<div class="card p-4 col-2 text-center text-white bg-black">
<img
v-show="filmData.poster_path != null"
:src="'http://image.tmdb.org/t/p/w342/' + filmData.poster_path"
:alt="filmData.title"
/>
<h3>{{ filmData.title }}</h3>
<h5
v-show="
filmData.title.toLowerCase() != filmData.original_title.toLowerCase()
"
>
{{ filmData.original_title }}
</h5>
<lang-flag
class="flag"
:iso="filmData.original_language"
:squared="false"
/>
<h4>{{ filmData.vote_average }}</h4>
<div>
<div class="star" v-for="element in fullArrayStars()" :key="element">
&starf;
</div>
<div
class="actor"
v-for="element in ricercaAttori /*()*/"
:key="element.name"
>
{{ element.name }}
</div>
</div>
</div>
</template>
<script>
import LangFlag from "vue-lang-code-flags";
import axios from "axios";
export default {
name: "CardBool",
data() {
return {
starf: "star",
arrayStars: [],
stars: Math.floor(this.filmData.vote_average / 2),
arrayAttori: null,
};
},
components: {
LangFlag,
},
props: {
filmData: Object,
},
methods: {
fullArrayStars() {
return this.stars;
},
ricercaAttori() {
axios
.get(
`https://api.themoviedb.org/3/movie/${this.filmData.id}/credits?api_key=9631e84004e35c8371fcb3c009af9551`
)
.then((response) => {
this.arrayAttori = response.data.cast;
});
this.arrayAttori.length = 5;
console.log(this.arrayAttori);
return this.arrayAttori;
},
},
};
</script>
i m working in VUE CLI...
thanks to everyone
methods: {
fullArrayStars() {
return this.stars;
},
changeVisibilita() {
if (this.visibilita == false) {
this.visibilita = true;
} else {
this.visibilita = false;
}
},
},
created() {
axios
.get(
`https://api.themoviedb.org/3/movie/${this.filmData.id}/credits?api_key=9631e84004e35c8371fcb3c009af9551`
)
.then((response) => {
this.arrayAttori = response.data.cast;
})
.then(() => {
this.arrayAttori.splice(5, this.arrayAttori.length);
});
return this.arrayAttori;
},
this is the solution... you have to use created() so you can do the call to the API before the creation of the Card, and you have to use then two times...
one time to create the array and the second time to work in that array, in this case to splice it

having problems with stripe/ redirecttocheckout

i have been trying to understand stripe for quite a while now. the major problem i am having is that i am a front end developer (with about a year of experience) and while i have some node.js/backend experience it is simply not enough to handle server processing of payments. i am going for the JAMstack serverless function approach using netlify. and thus far everything seems to be working out EXCEPT right at redirect to checkout i am getting the error "stripe.redirectToCheckout is not a function"
here is some of my code :
const inventory = require('./data/products.json');
exports.handler = async (event) => {
const { sku, quantity } = JSON.parse(event.body);
const product = inventory.find((p) => p.sku === sku);
const validatedQuantity = quantity > 0 && quantity < 2 ? quantity : 1;
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
billing_address_collection: 'auto',
shipping_address_collection: {
allowed_countries: ['US'],
},
success_url: `${process.env.URL}/success`,
cancel_url: process.env.URL,
line_items: [
{
name: 'bitch',
currency:'USD',
amount: 299,
quantity: validatedQuantity,
},
],
});
return {
statusCode: 200,
body: JSON.stringify({
sessionId: session.id,
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
}),
};
};
^^^^this is where i create the checkout through a serverless function
although it took some time i have been able to create a lambda function through netlify, hide my public and private keys, create a stripe element, but i am just so confused as to why i am getting this error...
blow is where the error seems to be
//client sides
import Stripe from 'stripe'
export async function handleFormSubmission(event) {
event.preventDefault();
const form = new FormData(event.target);
const data = {
sku: form.get('sku'),
quantity: Number(form.get('quantity')),
};
const response = await fetch('/.netlify/functions/create-checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}).then((res) => res.json());
const stripe=Stripe(response.publishableKey);
const {err}=await stripe.redirectToCheckout({
sessionId:response.sessionId
})
if(err){
console.log(err)
}
}
also if it is any help here is where i am calling the function (handleformsubmit or whatever)
import React from 'react'
import {loadStripe} from "#stripe/stripe-js"
import './Checkout.css'
import {useState} from 'react'
import {Elements,
CardElement,
useStripe,
useElements} from '#stripe/react-stripe-js'
import axios from 'axios'
import {loadProducts} from './load-products'
import {handleFormSubmission} from './stripe-purchase'
const stripePromise=loadStripe(`${process.env.STRIPE_}`)
const CheckoutForm=()=>{
const stripe=useStripe()
const elements=useElements()
const handleSubmit=async(e)=>{
e.preventDefault()
if (!stripe || !elements) {
// Stripe.js has not loaded yet. Make sure to disable
// form submission until Stripe.js has loaded.
return;
}
// Get a reference to a mounted CardElement. Elements knows how
// to find your CardElement because there can only ever be one of
// each type of element.
const cardElement = elements.getElement(CardElement);
const {error, paymentMethod}=await stripe.createPaymentMethod({
type:'card',
card:cardElement
})
loadProducts();
}
return (
<form onSubmit={handleFormSubmission} method='POST'>
<img className='checkoutImage' src='./logo2.png' />
<label class="ml-2 font-bold text-blue-700 text-md">
Enter card details below:
</label>
<fieldset className="my-2 FormGroup">
<div className="FormRow">
<CardElement options={{
style: {
base: {
fontSmoothing: 'antialiased',
fontWeight: 900,
iconColor: '#60A5FA',
fontSize: '30px',
color: '#374151',
'::placeholder': {
color: '#3182ce',
},
},
invalid: {
iconColor: '#EF4444',
color: '#DC2626',
},
complete:{
iconColor:'green',
color: 'green',
}
},
}}/>
</div>
</fieldset>
<div className='checkoutbuttonContainer'>
<button type="submit" className="scoreButtons scoreButtonGrey flex justify-center rounded-md border border-gray-300 bg-pink-600 shadow-sm px-4 py-2 bg-white text-base font-medium text-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 hover:bg-pink-500 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Submit Payment <span className='priceLine'></span> <span className='price'> </span>
</button>
</div>
</form>
)
}
const StripeSetup = () => {
return (
<div><Elements stripe={stripePromise}><CheckoutForm /></Elements></div>
)
}
export default StripeSetup
this is the first time in a while i have felt really overwhelmed by new material. im not even sure if i'm learning at a proper pace anymore nor even learning as much as just copying in code i find online lol... but this did take a good amount of work on my end nonetheless. would really love if someone could help. BTW i am now realizing that i created stripe elements etc which i don't even know if are necessary when using redirect to checkout as this is supposed to lead the client to a stripe checkout? can someone clarify all this for me. and please help if they can! thanks so much in advance
ONE last thing i wanted to say. i do not need a cart, products listing or anything. this is a one time payment for 2.99 and it will lead to the next page is the user submits the payment. not sure if that changes anything but figured the more details the better
Problem was in AWFUL documentation as well as netlify's instructions.
i needed to call loadstripe again first:
const stripe=await loadStripe(response.publishableKey);
const {err}=await stripe.redirectToCheckout({
sessionId:response.sessionId
})

Laravel 5 vue.js: Property or method "comment" is not defined on the instance but referenced during render

I am creating commenting system using vue.js and laravel5.8.
I have done with models and seeding, so I have now 10 comments to one post (id is 51).
But I got this error,
Property or method "comment" is not defined on the instance but
referenced during render
and
Cannot read property 'user' of undefined
I have problems with fetching data.
I created a new endpoint for a comment function.
web.php
Route::get('results/{post}', 'ResultsController#show')->name('posts.show');
Route::get('results/{post}/comments', 'CommentsController#index');
I want to show comments in show.blade.php.
ResultsController.php
public function show(Post $post)
{
$recommended_posts = Post::latest()
->whereDate('date','>',date('Y-m-d'))
->where('category_id','=',$post->category_id)
->where('id','!=',$post->id)
->limit(7)
->get();
$posts['particular_post'] = $post;
$posts['recommended_posts'] = $recommended_posts;
$post->comments()->with('user')->get();
return view('posts.show',compact('posts'));
}
show.blade.php
<comments-component :post="{{ $posts['particular_post']->comments }}"></comments-component>
comments.vue
<div class="reply-comment" :v-for="comment in comments">
<div class="user-comment" >
<div class="user">
<!--<img src="" alt="" >-->
<avatar :username="comment.user.name" :size="30" ></avatar>
</div>
<div class="user-name">
<span class="comment-name">{{ comment.user.name }}</span>
<p> {{ comment.body }} </p>
</div>
</div>
<div class="reply">
<div class="seemorecomments">
see more
</div>
<button class="reply-button">
<i class="fas fa-reply"></i>
</button>
</div>
</div>
<script>
import Avatar from 'vue-avatar'
export default {
props: ['post'],
components: {
Avatar
},
mounted() {
this.fetchComments()
},
data: () => ({
comments: {
data: []
}
}),
methods: {
fetchComments() {
axios.get(`/results/${this.post.id}/comments`).then(({data}) => {
this.comments = data
})
}
}
}
CommentsController.php
public function index(Post $post)
{
return $post->comments()->paginate(5);
$post->comments()->with('user')->get();
}
comment.php
protected $with = ['user'];
I cannot get data object here.
Within axios, you may need to access data from the response that is returned (see console.log examples here), try the following within your comments component:
methods: {
fetchComments() {
axios.get(`/results/${this.post.id}/comments`).then((response) => {
this.comments = response.data.data
})
}
}
Note response.data.data is used.
I assume returning the ->paginate() will put the results within a data key in the returned array. If not, then just use response.data.
Also, in the controller getting the comments change to the following:
public function index(Post $post)
{
return $post->comments()->with('user')->paginate(5);
}
This will eager load the users with the queried comments.

Categories