I'm having trouble creating pagination with vue. My task is to make sure that when you click on the numbers of the buttons, new tasks from jsonplaceholder are loaded.
I have successfully loaded the first and second page. I assume this is related directly to my this.fetchTodos() action. I'm just learning vue and I need help figuring out how to update the data when moving to a new page without loading.
In this case, it is necessary that the url of the page changes (get request). My page state is changing, but posts are not loading when clicking on the third page.
Below is the code of four files that I think will help you understand the situation.
Maybe you will easier with GitHub, please check pagination branch
Thanks in advance for your help! If you have questions or need more information, write in the comments
TodoListView.vue - is starting page, where is todos fetching and rendered on page.
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template>
<div class="todolist">
<ContainerBootstrap>
<div class="row">
<div class="col-12 text-center">
<TitlePage v-if="todos" text="Список задач"/>
<TitlePage v-else text="Список задач пуст, создайте новую!"/>
<button-bootstrap data-bs-toggle="modal" data-bs-target="#createTodo" css-class="btn-lg btn-primary mt-2 mb-4">Создать задачу</button-bootstrap>
<ModalBootstrap #create="createTodo" :todos="todos" css-id="createTodo"/>
<SearchBootstrap v-if="todos" #search="searchTodo"/>
<div v-if="todos" class="d-flex justify-content-end mt-2">
<button-bootstrap #click.native="setCompletedToAllTodo()" css-class="btn-lg btn-success">Отменить всё как "Выполненные"</button-bootstrap>
</div>
</div>
</div>
<TodoList v-if="todos" :todos="searchedTodos"/>
<PaginationBootstrap :page="page" :total-pages="totalPages" class="mt-4"/>
</ContainerBootstrap>
</div>
</template>
<script>
import ContainerBootstrap from "#/components/UI/ContainerBootstrap";
import TitlePage from "#/components/TitlePage";
import TodoList from "#/components/TodoList";
import {mapState, mapActions, mapMutations, mapGetters} from 'vuex'
import ButtonBootstrap from "#/components/UI/ButtonBootstrap";
import ModalBootstrap from "#/components/UI/ModalBootstrap";
import SearchBootstrap from "#/components/UI/SearchBootstrap";
import PaginationBootstrap from "#/components/UI/PaginationBootstrap";
export default {
name: "TodoListView",
components: {
PaginationBootstrap,
SearchBootstrap, ModalBootstrap, TodoList , ButtonBootstrap, TitlePage, ContainerBootstrap},
data: function() {
return {
isShow: false,
}
},
methods: {
...mapActions({
fetchTodos: "todos/fetchTodos"
}),
...mapMutations({
setSearchQuery: 'todos/setSearchQuery'
}),
createTodo(todo) {
this.$store.commit('todos/addTodo', todo);
},
setCompletedToAllTodo() {
console.log('hello')
this.$store.commit('todos/setCompletedToAllTodo')
},
searchTodo(query) {
this.$store.state.todos.searchQuery = query;
}
},
mounted() {
this.fetchTodos()
},
computed: {
...mapState({
todos: state => state.todos.todos,
isTodosLoading: state => state.todos.isTodosLoading,
page: state => state.todos.page,
limit: state => state.todos.limit,
totalPages: state => state.todos.totalPages,
searchQuery: state => state.todos.searchQuery
}),
...mapGetters({
searchedTodos: 'todos/searchedTodos'
})
}
}
</script>
TodoListPaginationView - is second file, where is loading second page and another when click on pagination.
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template>
<div class="todolist">
<ContainerBootstrap>
<div class="row">
<div class="col-12 text-center">
<TitlePage :text="'Страница №'+ page"/>
<router-link to="/todolist">
<button-bootstrap css-class="btn-lg btn-primary mt-2 mb-4">Вернуться к началу</button-bootstrap>
</router-link>
</div>
<TodoList v-if="todos" :todos="searchedTodos"/>
<PaginationBootstrap :page="page" :total-pages="totalPages" class="mt-4"/>
</div>
</ContainerBootstrap>
</div>
</template>
<script>
import ContainerBootstrap from "#/components/UI/ContainerBootstrap";
import TitlePage from "#/components/TitlePage";
import ButtonBootstrap from "#/components/UI/ButtonBootstrap";
import TodoList from "#/components/TodoList";
import {mapActions, mapGetters, mapMutations, mapState} from "vuex";
import PaginationBootstrap from "#/components/UI/PaginationBootstrap";
export default {
name: "TodoListPaginationView",
components: {PaginationBootstrap, TodoList, ButtonBootstrap, TitlePage, ContainerBootstrap},
methods: {
...mapActions({
fetchTodos: "todos/fetchTodos",
}),
...mapMutations({
setSearchQuery: 'todos/setSearchQuery'
})
},
computed: {
...mapState({
todos: state => state.todos.todos,
isTodosLoading: state => state.todos.isTodosLoading,
page: state => state.todos.page,
limit: state => state.todos.limit,
totalPages: state => state.todos.totalPages,
searchQuery: state => state.todos.searchQuery
}),
...mapGetters({
searchedTodos: 'todos/searchedTodos'
})
},
mounted() {
this.fetchTodos()
},
}
</script>
PaginationBootstrap.vue - third file, where is logic for pagination. Ui bootstrap 5 file.
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template>
<nav aria-label="Page navigation example">
<ul class="pagination">
<li class="page-item"><a class="page-link" href="#">Предыдущая</a></li>
<li v-for="pageNumber in totalPages" :key="pageNumber" :class="{'active' : page === pageNumber}" class="page-item">
<span #click="changePage(pageNumber)" class="page-link">{{pageNumber}}</span>
</li>
<li class="page-item"><a class="page-link" href="#">Далее</a></li>
</ul>
</nav>
</template>
<script>
export default {
name: "PaginationBootstrap",
props: {
page: Number,
totalPages: Number
},
methods: {
changePage(pageNumber) {
this.$store.commit('todos/setPage', pageNumber);
if (pageNumber === 1) {
this.$router.push('/todolist')
}
else {
this.$router.push({name: 'todolistPagination', params: {page: pageNumber}})
}
}
}
}
</script>
<style lang="scss" scoped>
.pagination {
.page-item {
.page-link {
cursor: pointer;
}
}
}
</style>
todosModule.js - last file, where is vuex logic for todos.
import axios from "axios";
export const todosModule = {
state: () => ({
todos: [],
page: 1,
limit: 10,
totalPages: 0,
isTodosLoading: false,
searchQuery: '',
}),
mutations: {
setTodos(state, todos) {
state.todos = todos
},
setPage(state, page) {
state.page = page
},
setTotalPages(state, totalPages) {
state.totalPages = totalPages
},
setLoadingTodos(state, bool) {
state.isTodosLoading = bool
},
setCompleted(state, completed) {
const index = state.todos.findIndex(todo => todo.id === completed.id);
state.todos[index].completed = completed.completed
},
setCompletedToAllTodo(state) {
state.todos.map(obj => {
obj.completed = true
})
},
removeTodo(state, id) {
const index = state.todos.findIndex(todo => todo.id === id)
state.todos.splice(index, 1)
},
addTodo(state, todo) {
state.todos.unshift(todo);
},
setTitle(state, tusk) {
const index = state.todos.findIndex(todo => todo.id === tusk.id);
state.todos[index].title = tusk.title
},
setSearchQuery(state, searchQuery) {
state.searchQuery = searchQuery;
}
},
actions: {
async fetchTodos({state, commit}) {
try {
commit('setLoadingTodos' , true)
const response = await axios.get('https://jsonplaceholder.typicode.com/todos', {
params: {
_page: state.page,
_limit: state.limit
}
})
commit('setTotalPages', Math.ceil(response.headers['x-total-count'] / state.limit))
commit('setTodos', response.data)
}
catch (e) {
console.log(e)
}
finally {
commit('setLoadingTodos', false)
}
},
async getCurrentPage({commit}, currentPage) {
try {
commit('setPage', currentPage)
}
catch (e) {
console.log(e);
}
}
},
getters: {
searchedTodos(state) {
return [...state.todos].filter(todo => todo.title.toLowerCase().includes(state.searchQuery.toLowerCase()))
},
},
namespaced: true
}
Okey, I found solution for myself.
Most importent thing is watcher. I added to TodoListPaginationView.vue next code:
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template>
<div class="todolist">
<ContainerBootstrap>
<div class="row">
<div class="col-12 text-center">
<TitlePage :text="'Страница №'+ page"/>
<router-link to="/todolist">
<button-bootstrap css-class="btn-lg btn-primary mt-2 mb-4">Вернуться к началу</button-bootstrap>
</router-link>
</div>
<TodoList v-if="todos" :todos="searchedTodos"/>
<PaginationBootstrap :page="page" :total-pages="totalPages" class="mt-4"/>
</div>
</ContainerBootstrap>
</div>
</template>
<script>
import ContainerBootstrap from "#/components/UI/ContainerBootstrap";
import TitlePage from "#/components/TitlePage";
import ButtonBootstrap from "#/components/UI/ButtonBootstrap";
import TodoList from "#/components/TodoList";
import {mapActions, mapGetters, mapMutations, mapState} from "vuex";
import PaginationBootstrap from "#/components/UI/PaginationBootstrap";
export default {
name: "TodoListPaginationView",
components: {PaginationBootstrap, TodoList, ButtonBootstrap, TitlePage, ContainerBootstrap},
methods: {
...mapActions({
fetchTodos: "todos/fetchTodos",
}),
...mapMutations({
setSearchQuery: 'todos/setSearchQuery'
})
},
computed: {
...mapState({
todos: state => state.todos.todos,
isTodosLoading: state => state.todos.isTodosLoading,
page: state => state.todos.page,
limit: state => state.todos.limit,
totalPages: state => state.todos.totalPages,
searchQuery: state => state.todos.searchQuery
}),
...mapGetters({
searchedTodos: 'todos/searchedTodos'
})
},
watch: {
page: function (val) {
if (val) {
this.fetchTodos()
}
},
},
mounted() {
this.fetchTodos();
},
}
</script>
Related
I have 2 buttons. One adds a movie to local storage, the other removes it from there. I made a function that basically switches the button. If the movie is added it shows "remove", if the movie's not been added it shows the button "add".
The function works but it doesn't know when the boolean changes so the button doesn't change. Someone explained that i should use watch property, but how am I supposed to watch an output of a function?
here is the code
<template>
<div>
<div class="card" v-for="movie in movies"
:key="movie.id">
{{movie.title}}
{{movie.release_date}}
<button v-show="!showButton(movie.id)" type="submit" #click="storeMovie(movie.id)" >
Aggiungi
</button>
<button v-show="showButton(movie.id)" type="submit" #click="removeMovie(movie.id)">
Rimuovi
</button>
</div>
<div class="card" v-for="favourite in watchlist"
:key="favourite.id">
{{favourite.title}}
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'HomeComp',
data () {
return {
movies: [],
watchlist: [],
movie: null,
}
},
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)
},
watch: {
switchButton(oldValue, newValue) {
if (oldValue != newValue) {
this.showButton(id) = true;
} //made an attempt here
}
},
methods: {
storeMovie(id) {
const favouriteMovie = this.movies.find(movie => movie.id === id )
this.watchlist.push(favouriteMovie);
localStorage.setItem("watchlist", JSON.stringify(this.watchlist));
},
removeMovie(id) {
const removedMovie = this.watchlist.find(movie => movie.id === id )
const indexMovie = this.watchlist.indexOf(removedMovie);
if (indexMovie > -1) {
this.watchlist.splice(indexMovie, 1);
}
localStorage.setItem("watchlist", JSON.stringify(this.watchlist));
},
showButton(id) {
const favouriteMovie = this.watchlist.find(movie => movie.id === id )
if (favouriteMovie && favouriteMovie.length > 0) {
return true
} else{
return false
}
}
},
}
</script>
<style scoped lang="scss">
</style>
A better approach would be to store the state of a movie being stored or not in the watchlist directly on the movie object.
Then use a computed to get the watchlist from the movie list instead of using two different arrays.
<template>
<div>
<div class="card" v-for="movie in movies" :key="movie.id">
{{movie.title}}
{{movie.release_date}}
<button v-show="!movie.toWatch" type="submit" #click="storeMovie(movie.id)">
{{ movie.toWatch ? 'Rimuovi' : 'Aggiungi' }}
</button>
</div>
<div class="card" v-for="favourite in watchList" :key="favourite.id">
{{favourite.title}}
</div>
</div>
</template>
<script>
export default {
name: 'HomeComp',
data() {
return {
movies: [],
}
},
computed: {
// Get the watchList from the movies list
watchList() {
return this.movies.filter(movie => movie.toWatch)
}
},
watch: {
watchList(newWatchList) {
// Update the localStorage whenever the list changes
localStorage.setItem("watchlist", JSON.stringify(newWatchList));
}
},
mounted() {
// your axios call
},
methods: {
storeMovie(id) {
const favouriteMovie = this.movies.find(movie => movie.id === id)
if (favouriteMovie) {
// just reverse the boolean
favouriteMovie.toWatch = !favouriteMovie.toWatch
}
},
},
}
</script>
I am working on a Vue 3 app. I have 3 nested components: a dropdown component, which is nested inside a navigation component, which is nested inside a content component.
The dropdown should filter posts inside the grandparent component Main.vue by author.
I tried to emit a getPostsByUser(user) method upwards, one component at a time.
In the grandchild component UsersDropdown.vue:
<template>
<div class="dropdown">
<button type="button" class="btn btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
{{ label }}
</button>
<ul v-if="usersData?.length" class="dropdown-menu">
<li v-for="user in usersData" :key="user.userId">
<a class="dropdown-item" #click="handleClick(user)">{{ user.first_name }} {{ user.last_name }}</a>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'UsersDropdown',
props: {
label: String,
usersData: Object,
user: Object
},
methods: {
handleClick(user) {
this.$emit('getPostsByUser', user.userId)
}
}
}
</script>
In the parent component Navigation.vue:
<template>
<div class="navigation">
<UsersDropdown
#getPostsByUser="$emit('getPostsByUser', user)"
:label='"All users"'
:usersData='users'
/>
</div>
</template>
<script>
import UsersDropdown from './UsersDropdown'
export default {
props: {
usersData: Object,
},
components: {
UsersDropdown,
},
emits: ['getPostsByUser'],
data() {
return {
users: [],
gateways: []
}
},
async mounted() {
// Users
await this.axios.get(`${this.$apiBaseUrl}/users`).then((response) => {
if (response.data.code == 200) {
this.users = response.data.data;
}
}).catch((errors) => {
console.log(errors);
});
}
}
</script>
In the grandparent component Main.vue:
<template>
<div class="main">
<Navigation
#getPostsByUser='getPostsByUser(user)'
:label='"All users"'
:usersData='users' />
</div>
</template>
<script>
import Navigation from './Ui/Navigation'
export default {
name: 'Main',
components: {
Navigation
},
props: {
title: String,
tagline: String,
},
data() {
return {
userId: '',
posts: [],
// more code
}
},
methods: {
getPostsByUser(user) {
// get user id
this.userId = user.userId;
},
}
}
</script>
The problem
For a reason I was unable to understand, the Chrome console throws the error:
Cannot read properties of undefined (reading 'userId')
Where is my mistake?
The issue is with your Navigation component. You need to declare props and components in export default {} for them to work and pass the data.
Add the below code to it:
components: {
UsersDropdown,
},
props: {
usersData: Object,
},
Also, update your usersData prop mapping to UsersDropdown component tag in the HTML part of Navigation.vue component.
Replace existing with:
<UsersDropdown
#getPostsByUser="$emit('getPostsByUser', user)"
:label="'All users'"
:usersData="usersData"
/>
++
In your `Navigation.vue` component, you need to declare a method to listen to the incoming `user` data and then emit the data again to the parent `Main.vue` component. You're trying to do it directly hence the undefined issue.
Update you `UsersDropdown` component tag as below:
<UsersDropdown
#getPostsByUser="getPostsByUser"
:label="'All users'"
:usersData="usersData"
/>
Along with that add the following method to the export default {} section of Navigation.vue component.
methods: {
getPostsByUser(user) {
// get user id
console.log(user, "js");
this.$emit("getPostsByUser", user);
},
},
And finally, update your #getPostsByUser event listener with just the respective function name. Passing of the argument isn't needed.
Replace #getPostsByUser="getPostsByUser(user)" with #getPostsByUser="getPostsByUser" in the Navigation tag of the HTML in Main.vue component.
For more clear understanding try this sandbox: https://codesandbox.io/s/vue-3-event-handling-nested-comps-s626kp?file=/src/App.vue
In UsersDropdown.vue you are passing userId and in Main.vue you are awaiting whole user object , try with this.userId = user;
const app = Vue.createApp({
el: "#demo",
props: {
title: String,
tagline: String,
},
data() {
return {
userId: '',
posts: [],
// more code
}
},
methods: {
getPostsByUser(user) {
this.userId = user;
},
}
})
app.component("Navigation", {
template: `
<div class="navigation">
<users-dropdown
#getpostsbyuser="getPostsByUser"
:label='"All users"'
:users-data='users'
></users-dropdown>
</div>
`,
props: ['usersData'],
data() {
return {
users: [],
gateways: []
}
},
async mounted() {
// Users
/*await this.axios.get(`${this.$apiBaseUrl}/users`).then((response) => {
if (response.data.code == 200) {
this.users = response.data.data;
}
}).catch((errors) => {
console.log(errors);
});*/
this.users = [{userId: 1, first_name: "aaa", last_name: "bbb"}, {userId: 2, first_name: "ccc", last_name: "ddd"}, {userId: 3, first_name: "eee", last_name: "fff"}]
},
methods: {
getPostsByUser(user) {
this.$emit('getpostsbyuser', user);
},
}
})
app.component("UsersDropdown", {
template: `
<div class="dropdown">
<button type="button" class="btn btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
{{ label }}
</button>
<ul v-if="usersData?.length" class="dropdown-menu">
<li v-for="user in usersData" :key="user.userId">
<a class="dropdown-item" #click="handleClick(user)">
{{ user.first_name }} {{ user.last_name }}
</a>
</li>
</ul>
</div>
`,
props: {
label: String,
usersData: Array,
},
methods: {
handleClick(user) {
this.$emit('getpostsbyuser', user.userId)
}
}
})
app.mount('#demo')
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap#5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap#5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<div class="main">
<navigation #getpostsbyuser='getPostsByUser' :label='"All users"'></navigation>
<p>user id: {{ userId }}</p>
</div>
</div>
i try to build a little clothing web shop with nuxtjs. You can choose the color on the details page. The details page represents a pice of clothing. The ColorMenu is a component. If you choose something a color, it will emit it back to the details page and will send a new details request to my backend.
However, changing the color only works if you don't choose another piece of clothing. If you choose another piece of clothing (so the route parameters will change) and choose another color in the menu, there is a always an error that it cannot load anything. it seems that it sends repeated requests until the request is blocked.
The details routes are built according to this scheme: localhost/details/{sellableId}/{ideaId}/{appearanceId}
Details Page:
<template>
<section class="section">
<div v-if="details">
<div class="columns">
<div class="column">
<ImageCaroussel :images="details.images"></ImageCaroussel>
</div>
<div class="column">
<h3>Farben</h3>
<ColorMenu
:appearances="productType.appearances"
:appearanceIds="details.appearanceIds"
></ColorMenu>
</div>
</div>
</div>
</section>
</template>
<script>
import { mapState } from 'vuex'
import Dropdown from '~/components/details/Dropdown.vue'
import ColorMenu from '~/components/details/ColorMenu.vue'
import ImageCaroussel from '~/components/details/ImageCaroussel.vue'
export default {
created() {
this.$nuxt.$on('selected', ($event) => (this.selected = $event))
this.$nuxt.$on('selectedColor', ($event) => this.setSelectedColor($event))
},
data() {
return {
modal: false,
selected: '',
selectedColor: '',
}
},
async asyncData({ store, params }) {
console.log('asyncfirst')
if (params.sellableId && params.appearanceId && params.ideaId) {
await store.dispatch('details/get_details', {
sellableId: params.sellableId,
appearanceId: params.appearanceId,
ideaId: params.ideaId,
})
let sellableId = params.sellableId
let appearanceId = params.appearanceId
let ideaId = params.ideaId
console.log('asyncsecond!')
return { sellableId, appearanceId, ideaId }
}
},
mounted() {
this.sellableId = this.$route.params.sellableId
this.appearanceId = this.$route.params.appearanceId
this.ideaId = this.$route.params.ideaId
console.log('Mounted!')
},
components: {
Dropdown,
ColorMenu,
ImageCaroussel,
},
computed: {
...mapState({
details: (state) => {
return state.details.details
},
currency: (state) => {
return state.sellable.currency
},
productType: (state) => {
return state.details.productType
},
}),
},
methods: {
checkout: async function (sellableId, size, appearanceId) {
let link = await this.$backendrepositories.basket.checkout(
sellableId,
size,
appearanceId
)
if (link.status === 200 && link.data) {
this.modal = true
setTimeout(() => {
window.location.href = link.data.link
}, 3000)
}
},
setSelectedColor: async function (event) {
this.selectedColor = event
await this.$store.dispatch('details/get_details', {
sellableId: this.sellableId,
appearanceId: this.selectedColor,
ideaId: this.ideaId,
})
},
},
}
</script>
ColorMenu Component:
<template>
<div>
<div
v-for="(cell, index) in appearances"
:key="index"
style="display: inline-block"
>
<label v-if="appearanceIds.includes(cell.id)" class="self-container">
<input type="radio" checked="checked" name="color" />
<span
class="checkmark"
:style="`background-color: ${cell.colors[0].value}`"
#click="select(cell.id)"
></span>
</label>
</div>
</div>
</template>
<script>
export default {
data: function () {
return {
selected: '',
}
},
props: ['appearances', 'appearanceIds'],
methods: {
select(select) {
this.selected = select
this.$nuxt.$emit('selectedColor', this.selected)
},
},
}
</script>
There is a live demo at https://akano-frontend.vercel.app/
So I create this cart page in Vue. I just don't understand how to update the cart page total price when the quantity of the item child component increase or decreases. If the item component quantity increase, of course, the total price must increase too.
Here's my cart parent component :
<template>
<div class="cart-container">
<h1 class="cart-title-page">Keranjang Anda</h1>
<div class="cart-item-container">
<cart-item v-for="(data, i) in cartData" :item="data" :key="i" />
</div>
<div class="cart-total-wrapper">
<div class="total-text-wrapper">
<p>Total</p>
</div>
<div class="total-amount-wrapper">
<p>Rp. 150.000.000</p>
</div>
</div>
</div>
</template>
<script>
import CartItem from '#/components/cart-item'
export default {
data() {
return {
cartData: [
{
product_name: 'vario ZS1',
price: 1000000,
url_thumbnail: 'https://cdn3.imggmi.com/uploads/2019/10/8/9e27ca9046031f6f21850be39b379075-full.png',
color: '#fff'
},
{
product_name: 'vario ZS1',
price: 1000000,
url_thumbnail: 'https://cdn3.imggmi.com/uploads/2019/10/8/9e27ca9046031f6f21850be39b379075-full.png',
color: '#fff'
},
{
product_name: 'vario ZS1',
price: 1000000,
url_thumbnail: 'https://cdn3.imggmi.com/uploads/2019/10/8/9e27ca9046031f6f21850be39b379075-full.png',
color: '#fff'
}
]
}
},
methods: {
getAllCartItem () {
this.$store.dispatch('cart/checkCartItem')
this.cartData = this.$store.state.cart.cartItem
}
},
created () {
this.getAllCartItem ()
},
components: {
'cart-item': CartItem
}
}
</script>
this is my cart item child component:
<template>
<div class="root-cart-item">
<div class="container-cart-left">
<div class="cart-img-wrapper">
<img :src="item.url_thumbnail" />
</div>
<div class="cart-title-wrapper">
<div class="title-wrapper">
<h3>{{ getProductbrand }}</h3>
<p>{{ item.product_name }}</p>
</div>
</div>
</div>
<div class="container-cart-right">
<div class="cart-amount-wrapper">
<number-input v-model="singleCart.amount" :min="1" :max="singleCart.stok" inline center controls></number-input>
</div>
<div class="cart-price-wrapper">
<p>{{ getProductTotalPrice }}</p>
</div>
<div class="cart-delete-wrapper">
<img src="../assets/delete.svg"/>
</div>
</div>
</div>
</template>
<script>
import ProductImage from './product-image'
import VueNumberInput from '#chenfengyuan/vue-number-input';
export default {
props: {
item: {
type: Object,
required: true
}
},
data () {
return {
singleCart: {
stok: 15,
amount: 1,
totalPrice: 0
}
}
},
computed: {
getProductbrand: function () {
let splittedName = this.item.product_name.split(' ')
return splittedName[0]
},
getProductTotalPrice: function () {
var x = this.singleCart.totalPrice.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".")
var totalPrice = `Rp. ${x}`
return totalPrice
}
},
watch: {
'singleCart.amount': {
handler: function () {
this.singleCart.totalPrice = this.singleCart.price * this.singleCart.amount
},
deep: true
}
},
components: {
'product-image': ProductImage,
'number-input': VueNumberInput
}
}
</script>>
and if anyone wondering, this is my cart store:
const state = {
cartItem: []
}
const getters = {
getAllCartItem: (state) => {
return state.cartItem
}
}
const mutations = {
updateCartItem: (state, cart) => {
state.cartItems = cart
}
}
const actions = {
checkCartItem: ({ commit }) => {
let item = JSON.parse(localStorage.getItem('cart'))
if (cart) {
commit('updateCartItem', item)
}
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
like I said, the problem should be quite simple, I just have to update the CSS class .total-amount-wrapper in the parent component, when the quantity in the child component increase or decreases. The total price in the child cart-item component is working, I just have to find a way to count every total price in the child cart-item component, and show it in the parent component.
For update the parent you must use the v-model approach or use the $emit.
In your code you must update the input to use the v-model or you must $emit an event when price change.
The first is simple and you must follow the tutorial you find in the link above, the second is below.
Child Component
watch: {
'singleCart.amount': {
handler: function () {
this.singleCart.totalPrice = this.singleCart.price * this.singleCart.amount
this.$emit("priceChanged", this.singleCart.totalPrice);
},
deep: true
}
}
Parent
<template>
..
<div class="cart-item-container">
<cart-item v-for="(data, i) in cartData" :item="data" :key="i"
#priceChanged="onPriceChanged" />
</div>
</template>
<script>
methods: {
..
onPriceChanged(value) {
this.total += value;
}
}
</scritp>
My Home.vue file:
<template>
<div>
<my-post
v-for="(post, index) in posts"
:post="post"
:index="index"
:key="post.id"
></my-post>
</div>
</template>
<script>
import Post from './Post.vue';
export default {
data() {
return {
posts: []
}
},
mounted() {
axios.get('http://localhost/mine/test')
.then(response => {
this.posts = response.data.posts;
})
.catch(error => {
// console.log(error);
})
},
components: {'my-post': Post}
}
</script>
My Post.vue file:
<template>
<div class="post">
<!-- The content of the post...
I want to count the number of likes for each post here something like this:
<p>{{likes.length}}</p> -->
</div>
</template>
<script>
export default {
props: ['post'],
data() {
return {}
}
}
</script>
The data that is got by axios.get('http://localhost/mine/test') is like this:
posts: Array [
{0:first_name:'example123',post_id:1},
{1:first_name:'example456',post_id:2},
{2:first_name:'example789',post_id:3},
],
likes: Array [
{0:first_name:'example1',post_id:1},
{1:first_name:'example2',post_id:1},
{2:first_name:'example3',post_id:1},
]
Note that they are separate. The likes are not the children of posts.
I set likes in props as posts but the issue is that it shows the number of likes exactly the same.
How can I get the number of likes for each post?
Thanks
Ideally you need to update your schema, such that each post have a likes object defining them separately.
Incase its not possible you can modify your code by doing this.
Add a likes field having all likes.
export default {
data() {
return {
posts: [],
likes:0
}
},
mounted() {
axios.get('http://localhost/mine/test')
.then(response => {
this.posts = response.data.posts;
this.likes = response.data.likes;
})
.catch(error => {
// console.log(error);
})
},
components: {'my-post': Post}
}
Use filter to add/pass [likes] prop with likes specific to each post.
<my-post
v-for="(post, index) in posts"
:post="post"
:likes="likes.filter(x => x.post_id==post.post_id)"
:index="index"
:key="post.id">
</my-post>
CODE SNIPPET
function callMe() {
var post = Vue.component("post", {
template: "<p>PostId={{post.post_id}} . Number of likes: {{likes.length}}</p>",
props: {
likes: Array,
post: Object
},
data() {
return{
numberOfLikes:0
}
},
methods: {
}
})
var vm = new Vue({
el: '#app',
template: '<p><post v-for="(postObj,index) in post.posts" :post="postObj" :likes="post.likes.filter(x => x.post_id==postObj.post_id)"></post></p>',
data(){
return {
likes:0,
post:{
posts: [
{first_name:'example123',post_id:1},
{first_name:'example456',post_id:2},
{first_name:'example789',post_id:3},
],
likes: [
{first_name:'example1',post_id:1},
{first_name:'example2',post_id:1},
{first_name:'example3',post_id:1},
{first_name:'example4',post_id:2},
]
}
}
}
})
}
callMe();
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js" type="text/javascript"></script>
<div id="app">
</div>
First you should add a "like_count" (or whatever you wish) to your db scheema. I assume you are using some kinda REST service that supports basic http methods ie GET,POST,PUT,DELETE.
Then just do a simple api call in your post component.
<template>
<div class="post">
<!-- Whatever you wish to stylize -->
<button class="add-like" #click="addLike"> +1 </button>
<p>{{currentLikes}}</p>
</div>
</template>
<script>
export default {
props: ['post'],
computed: {
currentLikes () {
return parseInt(this.post.like_count) + 1
}
},
methods: {
addLike () {
axios.put('/yourlink/' + this.post.post_id, {
like_count: this.currentLikes
})
}
}
}
</script>