i am using a v-for to display list of product from an api request, the product card contains three buttons, one of the Adds item to cart,with a shopping-cart icon.
i want it so that when a user clicks the add to cart button, the shopping-cart icon changes to a spinner icon
I try declaring a "loading" in the data object, default set to false, so in my add to cart function, before the function is called, loading is set to true,
And in my template i use a v-show="loading" which set the visibility of the fa-spin to true if loading is true
//template
<template>
<div class="row">
<div v-for="product in products" v-bind:key="product_slug"
class="col-md-auto mx-auto card text-center card-product">
<div class="card-product__img">
<img class="card-img" src="img/product/product1.png" alt="">
<ul class="card-product__imgOverlay">
<li>
<button><i class="ti-search"></i></button>
</li>
<li>
<button #click="addToCart(product.id, product.slug, product.price)"><i
class="ti-shopping-cart"></i> <i v-show="loading" class="fa fa-spinner fa-spin"></i>
</button>
</li>
<li>
<button><i class="ti-heart"></i></button>
</li>
</ul>
</div>
<div class="card-body">
<p>Accessories</p>
<h4 class="card-product__title">{{ product.slug }}</h4>
<p class="card-product__price">₦ {{ product.price}}</p>
</div>
</div>
//script
<script>
export default {
data() {
return {
loading: false,
products: [],
product: {
"id": '',
"slug": '',
"product_image_1": '',
"product_image_2": '',
"product_image_3": '',
"product_image_4": '',
"price": '',
"qty": '',
"stock_status": '',
"sku": '',
"short_description": '',
"description": '',
},
product_slug: '',
pagination: {},
}
},
created() {
this.fetchProduct();
},
methods: {
fetchProduct(page_url) {
//assign variable to this
let vm = this;
// check if page url exist, = page url else = /api/shop
page_url = page_url || '/api/shop';
fetch(page_url)
.then(res => res.json())
.then(res => {
this.products = res.data;
vm.makePagination(res.links, res.meta);
})
.catch(err => console.log(err));
},
makePagination(links, meta) {
//Make an object made up of meta, page details from the api response
let pagination = {
current_page: meta.current_page,
last_page: meta.last_page,
next_page_url: links.next,
prev_page_url: links.prev,
};
// Set the object to the pagination value
this.pagination = pagination;
},
addToCart(id, slug, price) {
this.loading = true;
axios.post('/api/cart', {
id: id,
name: slug,
price: price,
})
.then(function (response) {
this.loading = false;
console.log(response.data);
})
.catch(function (err) {
this.loading = false;
this.addToCart = err;
});
}
}
}
</script>
The problems are
1) Once the add to cart button is clicked, the spinner shows in all of the product's card.
2) fa-cart icon is not hiding, shows side-by-side with the shopping-cart icon
3) fa-spin continues, even after success of api request
You need to maintain a dictionary of the loading state. In addToCart function, you need to set true for particular product id. Try this code.
addToCart(id, slug, price) {
this.loading[id] = true;
axios.post('/api/cart', {
id: id,
name: slug,
price: price,
})
.then(function (response) {
this.loading[id] = false;
console.log(response.data);
})
.catch(function (err) {
this.loading[id] = false;
this.addToCart = err;
});
}
In Fetch product function made some changes.
fetchProduct(page_url) {
//assign variable to this
let vm = this;
// check if page url exist, = page url else = /api/shop
page_url = page_url || '/api/shop';
fetch(page_url)
.then(res => res.json())
.then(res => {
this.products = res.data;
this.products.filter(function (item) {
vm.loading[item.id]=false;
return item;
})
vm.makePagination(res.links, res.meta);
})
.catch(err => console.log(err));
},
html changes.
<button #click="addToCart(product.id, product.slug, product.price)"><i
class="ti-shopping-cart"></i> <i v-show="loading[product.id]" class="fa fa-spinner fa-spin"></i>
</button>
Related
I have a js code that works, but the problem is that when I use it on the main HTML page, for some reason it only works on the first picture, although the LIKE icon is on all the pictures.
I do not understand why it is used only on the first value of the icon and not on the others.
How to make it work on all LIKE icons?
const likeIcon = document.getElementById('like-icon');
const likeCount = document.getElementById('like-count');
likeIcon.onclick = () => {
const newId = likeIcon.getAttribute('data-news');
const url = `/like_news/${parseInt(newId)}/`;
fetch(url, {
method: 'GET',
headers: {
'Content-type': 'applicatin/json'
}
})
.then(response => {
return response.json();
})
.then(data => {
if(data.liked) {
likeIcon.classList.remove('empty-heart');
}
else {
likeIcon.classList.add('empty-heart');
}
likeCount.innerHTML = data.like_count;
})
.catch(error => {
console.log(error);
})
}
HTML CODE
<section class="trend">
<div class="container">
<div class="flex align-center justify-between trend-header">
<h2 class="subtitle">
<i class="ri-fire-line subtitle-icon"></i>
В тренде
</h2>
<button class="btn btn-outline">Посмотреть все</button>
</div>
<div class="trend-content">
{% for new in news|slice:"2" %}
<div class="trend-card">
<img src="{{new.banner.url}}" alt="newsPhoto" class="trend-background" />
<div class="card-header">{{new.category.title}}</div>
<div class="card-bottom">
<h3 class="card-title">
{{new.title}}
</h3>
<div class="card-btn">
<div class="count" id="like-count">{{new.likes.count}}</div>
{% if liked_by %}
<button class="btn-up" class="fa fa-heart">
<li><i id="like-icon" data-news="{{new.id}}" class="fa fa-heart"></i></li>
</button>
{% else %}
<button class="btn-up" class="fa fa-heart empty-heart">
<li><i id="like-icon" data-news="{{new.id}}" class="fa fa-heart empty-heart"></i></li>
</button>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</section>
Tried to use https://developer.mozilla.org/en/docs/Web/API/Document/querySelectorAll but couldn't do it
const likeIcon = document.querySelectorAll('#like-icon');
const likeCount = document.getElementById('like-count');
likeIcon.forEach(like-icon => {
like-icon.addEventListener("click", () => {
const newId = likeIcon.getAttribute('data-news');
const url = `/like_news/${parseInt(newId)}/`;
fetch(url, {
method: 'GET',
headers: {
'Content-type': 'applicatin/json'
}
})
.then(response => {
return response.json();
})
.then(data => {
if(data.liked) {
likeIcon.classList.remove('empty-heart');
}
else {
likeIcon.classList.add('empty-heart');
}
likeCount.innerHTML = data.like_count;
})
.catch(error => {
console.log(error);
})
});
}
}
And i expected it to work for all pictures that I had on main HTML page
It looks like you're on the right track using querySelectorAll to get all the like icons. However, there is a small mistake in your code when you use likeIcon instead of like-icon in your forEach loop.
Try replacing your JS code with the following, which should work for all like icons:
const likeIcons = document.querySelectorAll('#like-icon');
const likeCounts = document.querySelectorAll('#like-count');
likeIcons.forEach((likeIcon, index) => {
likeIcon.addEventListener('click', () => {
const newId = likeIcon.getAttribute('data-news');
const url = `/like_news/${parseInt(newId)}/`;
fetch(url, {
method: 'GET',
headers: {
'Content-type': 'application/json'
}
})
.then(response => {
return response.json();
})
.then(data => {
if (data.liked) {
likeIcons[index].classList.remove('empty-heart');
} else {
likeIcons[index].classList.add('empty-heart');
}
likeCounts[index].innerHTML = data.like_count;
})
.catch(error => {
console.log(error);
});
});
});
This code should loop through all the like icons and add a click event listener to each of them. When a user clicks on a like icon, the corresponding like count and icon should update based on the server response.
Note that we are using querySelectorAll to get all the like icons and like counts, and then we are looping through them using forEach. We are also using the index of each like icon to update the correct like count and icon.
You need to use class selector instead of ID selector.
While multiple Dom have the same ID, but only the first Dom can be query by document.querySelectorAll or document.querySelector.
Selects an element based on the value of its id attribute. There should be only one element with a given ID in a document.
https://developer.mozilla.org/en-US/docs/Web/CSS/ID_selectors
<i id="like-icon" data-news="{{new.id}}" class="fa fa-heart like-icon"></i>
const likeIcon = document.querySelectorAll('.like-icon');
// like-icon is not a right variable definition.
likeIcon.forEach(likeIcon => {
likeIcon.addEventListener("click", () => {
...
I don't get how i am supposed to save only a single object a not the whole array. I am trying to create a movie watchlist. If I click "add to watchlist", the single object should be saved in LocalStorage. If I hit remove from watchlist, the object should get removed. I tried to write down methods to regulate all of that, but i guess somethings wrong. The data comes from an API request. Here's the code:
<template>
<div>
<div class="card" v-for="movie in movies"
:key="movie.id">
{{movie.title}}
{{movie.release_date}}
<button type="submit" #click="storeMovie" >
Aggiungi
</button>
<button type="submit" #click="removeMovie">
Rimuovi
</button>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
//Cambiare il nome con quello del componente creato
name: 'HomeComp',
data () {
return {
movies: [],
movie: "",
}
},
mounted () {
axios
.get('https://api.themoviedb.org/3/movie/popular?api_key=###&language=it-IT&page=1&include_adult=false®ion=IT')
.then(response => {
this.movies = response.data.results
// console.log(response.data.results)
})
.catch(error => {
console.log(error)
this.errored = true
})
.finally(() => this.loading = false)
if (localStorage.movies) {
this.movies = JSON.parse(localStorage.movies);
}
},
watch: {
movies: {
handler(newMovies) {
localStorage.movies = JSON.stringify(newMovies);
},
deep:true
}
},
methods: {
getMovie() {
this.movies = JSON.parse(localStorage.getItem("movie"));
},
storeMovie() {
if (this.movie.length) {
// push the new movie to list
this.movies.push(this.movie);
// store the data in localStorage
localStorage.setItem("movies", JSON.stringify(this.movies));
// clear the input
this.movie = "";
}
},
removeMovie() {
localStorage.removeItem('movie');
}
},
}
</script>
<style scoped lang="scss">
/*Inserire style componente*/
</style>
tried to parse ad stringify, but i think i'm doing it wrong in some way. Also written some methods, not working
Few observations as per the code you posted :
As you want to store the new movie through input, Aggiungi button should come outside of v-for loop.
For removeStore event, You need to pass the store id from a template so that we can filter out the movies array.
Live Demo :
new Vue({
el: '#app',
data: {
movies: [],
movie: ''
},
mounted() {
// This data will come from API, Just for a demo purpose I am using mock data.
this.movies = [{
id: 1,
title: 'Movie A',
release_date: '06/12/2022'
}, {
id: 2,
title: 'Movie B',
release_date: '07/12/2022'
}, {
id: 3,
title: 'Movie C',
release_date: '08/12/2022'
}, {
id: 4,
title: 'Movie D',
release_date: '09/12/2022'
}, {
id: 5,
title: 'Movie E',
release_date: '10/12/2022'
}]
},
methods: {
storeMovie() {
const newMovieID = this.movies.at(-1).id + 1;
this.movies.push({
id: newMovieID,
title: this.movie,
release_date: '06/12/2022'
})
},
removeMovie(movieID) {
this.movies = this.movies.filter(({ id }) => id !== movieID)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div>
Add new movie : <input type="text" v-model="movie"/>
<button type="submit" #click="storeMovie()">
Aggiungi
</button>
</div><br>
<div class="card" v-for="movie in movies"
:key="movie.id">
{{movie.title}}
{{movie.release_date}}
<button type="submit" #click="removeMovie(movie.id)">
Rimuovi
</button>
</div>
</div>
but i can't show the comments with v-for and i don't understand why my comment data is not working.
I know there is an error but I can't find it.
My request returns a data , but i can't display it my loop.
Thanks for your help
In store/index.js
state :{
dataComments:[]
}
mutation: {
getComments(state, dataComments) {
console.log(dataComments)
state.dataComments = dataComments;
},
}
action: {
getArticleComments: ({ commit }, dataArticles) => {
return new Promise(() => {
instance.get(`/comment/${dataArticles.article_id}`)
.then(function () {
commit('getComments');
})
.catch(function (error) {
console.log(error)
})
})
},
}
in my views/home.vue
export default {
name: "Home",
data: function () {
return {
articles: [],
comments: [],
}
},
methods: {
getArticleComments(comment) {
this.$store
.dispatch("getArticleComments",comment)
.then((res) => {
this.comments = res.data;
});
},
}
<div class="pos-add">
<button
#click="getArticleComments(article)"
type="button"
class="btn btn-link btn-sm">
Show comments
</button>
</div>
<!-- <div v-show="article.comments" class="container_comment"> -->
<div class="container_comment">
<ul class="list-group list-group comments">
<li
class="
list-group-item
fst-italic
list-group-item-action
comment
"
v-for="(comment, indexComment) in comments"
:key="indexComment"
>
{{ comment.comment_message }}
<!-- {{ comment.comment_message }} -->
</li>
</ul>
</div>
Your action getArticleComments does not return anything and I would avoid changing the action to return data. Instead remove the assignment to this.comments in home.vue
Actions do not return data, they get data, and call mutations that update your store.
Your store should have a getter that exposes the state, in this case the dataComments.
getters: {
dataComments (state) {
return state.dataComments;
}
}
Then in your home.vue you can use the helper mapGetters
computed: {
...mapGetters([
'dataComments'
])
}
You want your views to reference your getters in your store, then when any action updates them, they can be reactive.
More here: https://vuex.vuejs.org/guide/getters.html
As far as I see, you don't return any data in your getArticleComments action. To receive the comments you should return them, or even better, get them from your store data directly.
First make sure that you pass the response data to your mutation method:
getArticleComments: ({ commit }, dataArticles) => {
return new Promise(() => {
instance.get(`/comment/${dataArticles.article_id}`)
.then(function (res) {
commit('getComments', res.data);
})
.catch(function (error) {
console.log(error)
})
})
},
After dispatching you could either return the response data directly or you could access your store state directly. Best practice would be working with getters, which you should check in the vue docs.
getArticleComments(comment) {
this.$store
.dispatch("getArticleComments",comment)
.then((res) => {
// in your case there is no res, because you do not return anything
this.comments =
this.$store.state.dataComments;
});
},
I am trying to complete a CRUD application with Vuejs and Laravel. Right now I can add an article but cannot delete and I ssee this error in my console:
SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
The html inside the template is like the following:
<div class="card card-body"v-for="article in articles" :key="article.id">
<h3>{{ article.title }}</h3>
<p>{{ article.body }}</p>
<button #click="deleteArticle(article.id)" class="btn btn-danger">Delete</button>
</div>
Then inside the script I have this:
How can I make the delete work?
<script>
export default {
data(){
return{
articles: [],
article: {
id: '',
title: '',
body: ''
},
article_id: '',
pagination: {},
edit: false
}
},
created(){
this.fetchAllArticles();
},
methods: {
fetchAllArticles(){
fetch('/api/articles').then(res => res.json()).then(res => {
this.articles = res.data;
})
.catch(err => console.log(err));
},
deleteArticle(id){
if(confirm('Are you sure?')){
fetch('api/article/${id}', {
method: 'delete'
})
.then(res => res.json())
.then(data => {
alert('Article removed');
this.fetchAllArticles();
})
.catch(err => console.log(err));
}
}
}
}
</script>
If I type my url for the delete method I can view the data as you can see in the image:
My delete controller looks like this:
public function destroy($id)
{
// Get article
$article = Article::findOrFail($id);
if($article->delete()) {
return new ArticleResource($article);
}
}
From the netwok in the browser I see this:
Thanks in advance
I have never use fetch i love using axios, but through googling i found that you have to use not those symbols '' but those `` so:
fetch(`api/article/${id}`, {
method: 'delete'
})
From what I see in your console the API address is incorrect, should be 'api/article/' + id
I'm new to Vue.js but trying to get to grips with it. So far it has gone well and I've got quite far but I am stuck extending a parents template.
I am trying to make dashboard widgets that extend a default widget layout (in Boostrap). Please note that the below code is using Vue, Require, Underscore & Axios.
Parent file - _Global.vue
<template>
<div class="panel panel-default">
<div class="panel-heading">
<b>{{ widgetTitle }}</b>
<div class="pull-right">
<a href="#" v-on:click="toggleMinimized"
v-bind:title="(isMinimized ? 'Show widget' : 'Hide widget')">
<i class="fa fa-fw" v-bind:class="isMinimized ? 'fa-plus' : 'fa-minus'"></i>
</a>
</div>
</div>
<div class="panel-body" v-if="!isMinimized">
<div class="text-center text-muted" v-if="!isLoaded">
<i class="fa fa-spin fa-circle-o-notch"></i><br />
</div>
<parent v-if="isLoaded">
<!-- parent content should appear here when loaded -->
</parent>
</div>
</div>
</template>
<script>
export default {
// setup our widget props
props: {
'minimized': {
'default': false,
'required': false,
'type': Boolean
}
},
// define our data
data: function () {
return {
widgetTitle: 'Set widget title in data',
isLoaded: false,
isMinimized: this.$props.minimized
}
},
// when vue is mounted, open our widget
mounted: function () {
if(!this.isMinimized) {
this.opened();
}
},
// define our methods
methods: {
// store our widget state to database
storeWidgetState: function () {
// set our data to send
let data = {
'action' : 'toggleWidget',
'widget' : this.$options._componentTag,
'state' : !this.isMinimized
};
// post our data to our endpoint
axios.post(axios.endpoint, data);
},
// toggle our minimized data
toggleMinimized: function (e) {
// prevent default
e.preventDefault();
// toggle our minimized state
this.isMinimized = !this.isMinimized;
// trigger opened if we aren't minimized
if(!this.isMinimized) this.opened();
// save our widget state to database
this.storeWidgetState();
},
// triggered when opened from being minimized
opened: function () {
console.log('opened() method is where all widget logic should be placed');
}
}
}
</script>
Child file - Example.vue
Should extend _Global.vue using mixins and then display content within .panel-body
<template>
<div>
I want this content to appear inside the .panel-body div
{{ content }}
<img v-bind:src="image.src" v-bind:alt="image.alt"
v-if="image.src" class="img-responsive" style="margin: 0 auto" />
</div>
</template>
<script>
// import our widgets globals
import Global from './_Global.vue'
export default {
components: {
'parent': {
// what can I possibly put here??
}
},
// use our global mixin for all widgets
mixins: [Global],
// setup our methods for this widget
methods: {
opened: _.debounce(function () {
// make sure this can only be opened once
if(this.hasBeenOpened) return;
this.hasBeenOpened = true;
// temporarily allow axios to make external requests
let axiosHeaders = axios.defaults.headers.common;
let vm = this;
axios.defaults.headers.common = {};
axios.get('https://yesno.wtf/api')
.then(function (res) {
// set our content
vm.content = null;
// set our image content
vm.image.src = res.data.image;
vm.image.alt = res.data.answer;
})
.catch(function (err) {
// set our error text
vm.content = String(err);
})
.then(function () {
// this will always hit..
vm.isLoaded = true;
});
// restore our axios headers for security
axios.defaults.headers.common = axiosHeaders;
}, 300)
},
// additional data
data: function () {
return {
// set our widgets title
widgetTitle: 'Test title',
// logic for the specific widget
hasBeenOpened: false,
content: 'Loaded and ready to go...',
image: {
src: false,
alt: null
}
};
},
}
</script>
Currently my parent template is just completely overwriting my child view. The only way I can get it to work is by explicitly defining the template parameter inside components -> parent: {} but I don't want to have to do that...?
Ok, thanks to Gerardo Rosciano for pointing me the in right direction. I've used to slots to come up with an eventual solution. We then access parent methods and data attributes just to get everything working as it should.
Example.vue - our example widget
<template>
<div>
<widget-wrapper>
<span slot="header">Example widget</span>
<div slot="content">
<img v-bind:src="image.src" v-bind:alt="image.alt"
v-if="image.src" class="img-responsive" style="margin: 0 auto" />
{{ content }}
</div>
</widget-wrapper>
</div>
</template>
<script>
// import our widgets globals
import WidgetWrapper from './_Widget.vue'
export default {
// setup our components
components: {
'widget-wrapper': WidgetWrapper
},
// set our elements props
props: {
'minimized': {
'type': Boolean,
'default': false,
'required': false
}
},
// setup our methods for this widget
methods: {
loadContent: _.debounce(function () {
// make sure this can only be opened once
if(this.hasBeenOpened) return;
this.hasBeenOpened = true;
// temporarily allow axios to make external requests
let axiosHeaders = axios.defaults.headers.common;
let vm = this;
axios.defaults.headers.common = {};
axios.get('https://yesno.wtf/api')
.then(function (res) {
// set our content
vm.content = null;
// set our image content
vm.image.src = res.data.image;
vm.image.alt = res.data.answer;
})
.catch(function (err) {
// set our error text
vm.content = String(err);
})
.then(function () {
// this will always hit..
vm.isLoaded = true;
});
// restore our axios headers for security
axios.defaults.headers.common = axiosHeaders;
}, 300)
},
// additional data
data: function () {
return {
// global param for parent
isLoaded: false,
// logic for the specific widget
hasBeenOpened: false,
content: 'Loaded and ready to go...',
image: {
src: false,
alt: null
}
};
},
}
</script>
_Widget.vue - our base widget that gets extended
<template>
<div class="panel panel-default">
<div class="panel-heading">
<b><slot name="header">Slot header title</slot></b>
<div class="pull-right">
<a href="#" v-on:click="toggleMinimized"
v-bind:title="(minimized ? 'Show widget' : 'Hide widget')">
<i class="fa fa-fw" v-bind:class="minimized ? 'fa-plus' : 'fa-minus'"></i>
</a>
</div>
</div>
<div class="panel-body" v-if="!minimized">
<div class="text-center text-muted" v-if="!isLoaded">
<i class="fa fa-spin fa-circle-o-notch"></i><br />
Loading...
</div>
<div v-else>
<slot name="content"></slot>
</div>
</div>
</div>
</template>
<script>
export default {
// get loaded state from our parent
computed: {
isLoaded: function () {
return this.$parent.isLoaded;
}
},
// set our data element
data: function () {
return {
minimized: false
}
},
// when the widget is mounted, trigger open state
mounted: function () {
this.minimized = this.$parent.minimized;
if(!this.minimized) this.opened();
},
// methods to manipulate our widget
methods: {
// save our widget state to database
storeWidgetState: function () {
// set our data to send
let data = {
'action' : 'toggleWidget',
'widget' : this.$parent.$options._componentTag,
'state' : !this.minimized
};
// post this data to our endpoint
axios.post(axios.endpoint, data);
},
// toggle our minimized state
toggleMinimized: function (e) {
// prevent default
e.preventDefault();
// toggle our minimized state
this.minimized = !this.minimized;
// trigger opened if we aren't minimized
if(!this.minimized) this.opened();
// save our widget state to database
this.storeWidgetState();
},
// when widget is opened, load content
opened: function () {
// make sure we have a valid loadContent method
if(typeof this.$parent.loadContent === "function") {
this.$parent.loadContent();
} else {
console.log('You need to define a loadContent() method on the widget');
}
}
}
}
</script>