Pagination for dynamically generated content - vue.js

Consider the following code.
<template>
<div class="card-container">
<div class="row">
<div class="col s12">
<a #click="addCard()">Add Card</a>
</div>
</div>
<div class="row">
<div v-for="(card, index) in cards" :key="index">
<div class="card-panel">
<span class="card-title">Card Title</span>
<div class="card-action">
<a #click="deleteCard(index)">Delete</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data: function() {
return {
cards: []
}
},
methods: {
addCard: function() {
this.cards.push({
description: "",
});
},
deleteCard: function(index) {
this.cards.splice(index,1);
}
},
}
</script>
How to make the cards be grouped so that there are 4 rows and each row contains 4 cards? Upon reaching the fourth row the new cards go to the next page.
I thought I could use something like this codepen.io/parths267/pen/bXbWVv
But I have no idea how to get these cards organized in a pagination system.
The view would look something like this

My solution is calculate all cards of current page ahead.
Uses computed property to calculate the relate values which the pagination needs.
In below simple example (it is only one example, you need to add necessary validations as your actual needs, like boundary conditions) :
pages is the page count
cardsOfCurPage is the cards in current page
Then add one data property=pageIndex save the index of current page.
Anyway, keep data-driven in your mind.
List all arguments your pagination needs,
then declare them in data property or computed property.
execute the necessary calculations in computed property or methods.
PS: I don't know which css framework you uses, so I uses bootstrap instead.
Vue.component('v-cards', {
template: `<div class="card-container">
<div class="row">
<div class="col-12">
<a class="btn btn-danger" #click="addCard()">Add Card</a><span>Total Pages: {{pages}} Total Cards: {{cards.length}} Page Size:<input v-model="pageSize" placeholder="Page Size"></span>
</div>
</div>
<div class="row">
<div v-for="(card, index) in cardsOfCurrPage" :key="index" class="col-3">
<div class="card-panel">
<span class="card-title">Card Title: {{card.description}}</span>
<div class="card-action">
<a #click="deleteCard(index)">Delete</a>
</div>
</div>
</div>
</div>
<p><a class="badge" #click="gotoPrev()">Prev</a>- {{pageIndex + 1}} -<a class="badge" #click="gotoNext()">Next</a></p>
</div>`,
data: function() {
return {
cards: [],
pageSize: 6,
pageIndex: 0
}
},
computed: {
pages: function () {
return Math.floor(this.cards.length / this.pageSize) + 1
},
cardsOfCurrPage: function () {
return this.cards.slice(this.pageSize * this.pageIndex, this.pageSize * (this.pageIndex+1))
}
},
methods: {
addCard: function() {
this.cards.push({
description: this.cards.length,
});
},
deleteCard: function(index) {
this.cards.splice(index,1);
},
gotoPrev: function() {this.pageIndex -=1},
gotoNext: function() {this.pageIndex +=1}
},
})
new Vue({
el: '#app',
data() {
return {
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" rel="stylesheet" />
<div id="app">
<v-cards></v-cards>
</div>

Related

How can I add a task to a list in my Vue 2 to-do app?

I am trying to add a task to a tasklist in Vue based on the input and add task button, but I keep getting the error "taskList is not defined". Does anybody see how to fix this problem? The code is as following:
<template>
<div id="input">
<form>
<input v-model="task.name">
<button v-on:click="addTask" v-bind:value="task.name">+</button>
</form>
<ol>
<div v-for="task in taskList" :key="task.id">
{{ task.name }}
<div v-if="task.completed">
<h2> Done </h2>
</div>
<div v-else>
<h2> Not done</h2>
</div>
</div>
</ol>
</div>
</template>
<script>
export default {
name: 'AddTask',
data: function() {
return {
taskList: [
{
name: 'task', completed: false, id: 3
}
] }
},
methods: {
addTask: function (task) {
taskList.push(task);
alert('test');
}
}
}
</script>
Ps. any other Vue tips are welcome as well.
You need to separate out your taskList and the current task you're adding, decouple it as a new object, then add it to your taskList array.
When referring to items in your data object you need to use the this keyword – e.g this.taskList rather than taskList:
new Vue({
el: "#app",
data: {
id:1,
taskList: [],
currentTask:{
completed:false,
name:'',
id:this.id
}
},
methods: {
addTask: function() {
let newTask = {
completed:this.currentTask.completed,
name:this.currentTask.name,
id:this.currentTask.id
}
this.taskList.push(newTask);
this.id++;
//alert('test');
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div id="input">
<ol>
<li v-for="task in taskList" :key="task.id">
{{ task.name }}
<input type="checkbox"
:checked="task.completed"
#change="task.completed = !task.completed">
<span v-if="task.completed">
Done
</span>
<span v-else>
Not Done
</span>
</li>
</ol>
<input type="text" v-model="currentTask.name">
<button v-on:click="addTask">+</button>
</div>
</div>
From what I see in your template you use tasklist but you define it as taskList You will want to make sure your names are in the same case. Usually you'll see camelCase in vue, but other popular ones are snake_case and PascalCase

Bind class item in the loop

i want to bind my button only on the element that i added to the cart, it's working well when i'm not in a loop but in a loop anything happen. i'm not sure if it was the right way to add the index like that in order to bind only the item clicked, if i don't put the index every button on the loop are binded and that's not what i want in my case.
:loading="isLoading[index]"
here the vue :
<div class="container column is-9">
<div class="section">
<div class="columns is-multiline">
<div class="column is-3" v-for="(product, index) in computedProducts">
<div class="card">
<div class="card-image">
<figure class="image is-4by3">
<img src="" alt="Placeholder image">
</figure>
</div>
<div class="card-content">
<div class="content">
<div class="media-content">
<p class="title is-4">{{product.name}}</p>
<p class="subtitle is-6">Description</p>
<p>{{product.price}}</p>
</div>
</div>
<div class="content">
<b-button class="is-primary" #click="addToCart(product)" :loading="isLoading[index]"><i class="fas fa-shopping-cart"></i> Ajouter au panier</b-button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
here the data :
data () {
return {
products : [],
isLoading: false,
}
},
here my add to cart method where i change the state of isLoading :
addToCart(product) {
this.isLoading = true
axios.post('cart/add-to-cart/', {
data: product,
}).then(r => {
this.isLoading = false
}).catch(e => {
this.isLoading = false
});
}
You can change your isLoading to an array of booleans, and your addToCart method to also have an index argument.
Data:
return {
// ...
isLoading: []
}
Methods:
addToCart(product, index) {
// ...
}
And on your button, also include index:
#click="addToCart(product, index)"
By changing isLoading to an empty array, I don't think isLoading[index] = true will be reactive since index on isLoading doesn't exist yet. So you would use Vue.set in your addToCart(product, index) such as:
this.$set(this.isLoading, index, true)
This will ensure that changes being made to isLoading will be reactive.
Hope this works for you.
add on data productsLoading: []
on add to cart click, add loop index to productsLoading.
this.productsLoading.push(index)
after http request done, remove index from productsLoading.
this.productsLoading.splice(this.productoading.indexOf(index), 1)
and check button with :loading=productsLoading.includes(index)
You can create another component only for product card,
for better option as show below
Kindly follow this steps.
place the content of card in another vue component as shown below.
<!-- Product.vue -->
<template>
<div class="card">
<div class="card-image">
<figure class="image is-4by3">
<img src="" alt="Placeholder image">
</figure>
</div>
<div class="card-content">
<div class="content">
<div class="media-content">
<p class="title is-4">{{product.name}}</p>
<p class="subtitle is-6">Description</p>
<p>{{product.price}}</p>
</div>
</div>
<div class="content">
<b-button class="is-primary" #click="addToCart(product)" :loading="isLoading"><i class="fas fa-shopping-cart"></i> Ajouter au panier</b-button>
</div>
</div>
</div>
</templete>
<script>
export default {
name: "Product",
data() {
return {
isLoading: false
}
},
props: {
product: {
type: Object,
required: true
}
},
methods: {
addToCart(product) {
this.isLoading = true
axios.post('cart/add-to-cart/', {
data: product,
}).then(r => {
this.isLoading = false
}).catch(e => {
this.isLoading = false
});
}
}
}
</script>
Change your component content as shown below.
<template>
<div class="container column is-9">
<div class="section">
<div class="columns is-multiline">
<div class="column is-3" v-for="(product, index) in computedProducts">
<product :product="product" />
</div>
</div>
</div>
</div>
</templete>
<script>
import Product from 'path to above component'
export default {
components: {
Product
}
}
</script>
so in the above method you can reuse the component in other components as well.
Happy coding :-)

Vue component data property not updating after parent data changes

I have a vue component (card-motor) with a prop named motor:
<div v-for="chunk in chunkDataMotores" class="row">
<div v-for="motor in chunk" class="col-md-6">
<card-motor :motor="motor"></card-motor>
</div>
</div>
Whenever data (motor) changes on the parent, the changes on the data property (id_color, id_motor, nombre _motor, etc...) of the component does not get updated. Here the card-motor component:
<template>
<div class="card" :data-motor-id="id_motor">
<div class="card-header" :style="backgroundColor">
<h4 class="text-center">{{nombre_motor}}<button class="btn btn-dark btn-sm pull-right" :data-motor-id="id_motor" #click="show_modal_colores(id_motor)">Color motor</button></h4>
</div>
<div class="card-body">
<div class="card">
<div class="card-header" role="tab" id="headingOne">
<div class="mb-0">
<a data-toggle="collapse" :href="computedId">
Piezas asociadas {{nombre_motor}} <i class="fa fa-caret-down" aria-hidden="true"></i>
</a>
<button #click="addPieza(id_motor)" class="btn pull-right" title="Añadir pieza nueva al motor"><i class="fa fa-plus text-info" aria-hidden="true"></i></button>
</div>
</div>
<div :id="id_motor" class="collapse" role="tabpanel" aria-labelledby="headingOne" data-parent="#accordion">
<div class="card-body">
<ul class="list-group">
<li class="list-group-item" v-for="pieza in piezas_motor">
<span class="badge badge-secondary">{{nombre_motor}}</span> {{pieza.pieza}}
<button class="btn btn-sm btn-danger pull-right"><i class="fa fa-trash" aria-hidden="true"></i></button>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['motor'],
data: function () {
return {
nombre_motor: this.motor.motor,
id_motor: this.motor.id,
id_color: this.motor.color.id,
piezas_motor: this.motor.piezas,
}
},
methods: {
show_modal_colores: function(id){
let $engine = $('#engine-colors');
$engine.data('motor-id', id);
$engine.find('div.color').removeClass('active');
$engine.find('div[data-id="'+this.activeColor+'"]').addClass('active');
$engine.modal('show');
},
addPieza(id) {
let $form = $('#form-pieza');
$form.data('motor-id', id);
$form.modal('show');
}
},
computed: {
computedId: function () {
return '#'+ this.id_motor;
},
backgroundColor: function () {
return 'background-color: '+ this.motor.color.codigo;
},
activeColor: function () {
return this.motor.color.id;
}
},
}
And here the parent code (root component):
Vue.component('card-motor', require('./components/CardMotor.vue'));
var app = new Vue ({
el: '#app',
data: {
dataMotores: [],
dataPuestos: [],
background_style: {
'background-color': ''
}
},
methods: {
makeActiveColor: function(e) {
$(e.currentTarget).closest('.modal-body').find('div.color').removeClass('active');
$(e.currentTarget).closest('div.color').addClass('active');
},
changeColor: function(e) {
let vm = this;
let id=$(e.currentTarget).closest('div.modal-content').find('.active').data('id');
let motor_id = $(e.currentTarget).closest('#engine-colors').data('motor-id');
axios.post('/admin/motores/change-color', {idmotor:motor_id, idcolor: id})
.then(response=>{
this.getData();
$('#engine-colors').modal('hide');
});
},
getData: function(){
axios.get('/admin/motores/api/data')
.then(response => {
this.dataMotores = response.data.motores;
this.dataPuestos = response.data.puestos;
})
.catch();
}
},
computed: {
chunkDataMotores() {
return _.chunk(this.dataMotores, 2);
}
},
created: function() {
this.getData();
}
});
Data returned from the axios call to the server are arrays of objects (getData method). Computed properties updates properly on the component, but not the data property.
You are making copies of your props, so the component renders, make your copies inside data(), but data() is called once, so when the parent component updates the child does not update.
data: function () {
return {
nombre_motor: this.motor.motor,
id_motor: this.motor.id,
id_color: this.motor.color.id,
piezas_motor: this.motor.piezas,
}
},
You can use motors prop directly, like:
<div class="card-header" :style="backgroundColor">
<h4 class="text-center">
{{ motor.motor }}
<button class="btn btn-dark btn-sm pull-right"
:data-motor-id="motor.id"
#click="show_modal_colores(motor.id)">
Color motor
</button>
</h4>
</div>
You need to pass value of dataMotores in components
<card-motor :motor="dataMotores"></card-motor>

Vue js event not picked up

I have just started experimenting with vue js and I am building a checkout form with it. I am also using Symfony 31 for the project. On the checkout/signup page I have an embedded collection of forms representing order items (each are subscription to a type of product). You can select multiple items by ticking a checkbox. You can also change the quantity. Unfortunately I cannot manage to pass the quantity update to the Vue instance. The entries are registered on render with the quantity 1, and if I change the quantity and then select the item, the price is calculated correctly, but the app registers this as a new entity. The binding with the quantity is not working. I will also need to add a similar field called frequency and I know I will have the same problem. Help?
Here is the js fiddle: https://jsfiddle.net/wavsu8xm/
Javascript:
var bus = new Vue();
var entriesComponent = Vue.component('entries', {
template: '#entries',
props: {
entries: [Array, Object],
selected: Array,
addons: Array,
frequencies: [Array, Object],
},
watch: {
selected: function(val, oldVal) {
bus.$emit('selected-changed', val);
},
}
});
new Vue({
el: '#app',
data: {
entries: [],
selected: [],
addons: [],
frequencies: [],
paymentConfig: {
advance: 25,
firstweek: 25,
ondelivery: 50,
},
weeks: 12,
},
components: {
'entriesComponent': entriesComponent,
},
created: function() {
// store this to use with Vue.set
var temp = this;
bus.$on('selected-changed', function(selected) {
// vm.$set deprecated
Vue.set(temp, 'selected', selected);
});
},
computed: {
totalAdvance: function() {
return (this.paymentConfig.advance * this.total) / 100;
},
totalFirstWeek: {
get: function() {
return (this.paymentConfig.firstweek * this.total) / 100;
},
},
onDeliveryPayment: {
get: function() {
return (this.paymentConfig.ondelivery * this.total) / (this.weeks * 100);
}
},
total: {
get: function() {
var sum = 0;
var weeks = this.weeks;
this.selected.forEach(function(item) {
sum += weeks * item.itemPrice * item.quantity;
});
console.log(sum);
return sum;
}
}
}
});
Template:
<section class="content">
<div class="row" id="app">
<div class="col-md-8">
<div class="box box-primary">
<div class="box-body">
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label class="control-label required">Items</label>
<div class="col-md-12">
<entries :entries="{ 0 : { shareSize : 'Small', quantity : '1', itemPrice : '24', frequency : '' }, 1 : { shareSize : 'Medium', quantity : '1', itemPrice : '35', frequency : '' }, 2 : { shareSize : 'Large', quantity : '1', itemPrice : '46', frequency : '' } }"
:selected="selected"></entries>
<!-- component template -->
<template id="entries">
<div class="col-md-12">
<div class="form-group" v-for="(entry, key) in entries" v-bind:entry="entry">
<div class="form-group col-md-12">
<div class="col-md-12">
<div class="col-md-4">
<input type="checkbox" v-bind:value="entry" v-model="selected">
</div>
<div class="col-md-4">{{entry.shareSize}}</div>
<div class="col-md-4">{{'$ ' + Number(entry.itemPrice).toFixed(2) }}</div>
</div>
<div class="form-group col-md-12">
<div class="col-md-6">
<input type="number" v-model="entry.quantity" :value="entry.quantity" />
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="box box-info">
<div class="box-body" style="padding:15px;">
<div class="container-fluid">
<div class="form-group">
<div class="control-label">
<label>Summary</label>
</div>
<div class="form-control" v-for="item in selected">
<span class="pull-left small-box-footer">{{ item.shareSize }}</span>
<span class="pull-right">{{ item.quantity + ' x $ ' + (item.itemPrice*item.quantity).toFixed(2)}}</span>
</div>
<div class="control-label">
<label>Payment plan</label>
</div>
<div class="col-md-12">
{{ '$ ' + totalAdvance.toFixed(2) }} - advance
</div>
<div class="col-md-12">
{{ '$ ' + totalFirstWeek.toFixed(2) }} - first week
</div>
<div class="col-md-12">
{{ '$ ' + onDeliveryPayment.toFixed(2) }}/ week on each of the {{ weeks }} weeks of the subscription
</div>
<div class="col-md-12 row">
<div class="control-label"><strong><span class="pull-left">Total</span><span class="pull-right">{{ '$ ' + total.toFixed(2) }}</span></strong></div>
</div>
<div class="col-md-12 row">
<div class="title"><strong><span class="pull-left">Total due now</span><span class="pull-right">{{ '$ ' + totalAdvance.toFixed(2) }}</span></strong></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
You are mutating your props directly which will get overwritten when the parent component re-renders, so you need to make a copy of them inside your component, which can be done inside the created method of your component:
created: function() {
// copy props to data
this.entriesCopy = this.entries;
this.selectedCopy = this.selected;
},
data: function() {
return{
entriesCopy: [],
selectedCopy: []
}
}
Now you just need to update your watcher:
watch: {
selectedCopy: function(val, oldVal) {
bus.$emit('selected-changed', val);
}
}
And your template:
//...
div class="form-group" v-for="(entry, key) in entriesCopy" v-bind:entry="entry">
//...
<input type="checkbox" v-bind:value="entry" v-model="selectedCopy">
to reflect the changes.
Here's the updated jsfiddle: https://jsfiddle.net/5pyw74h9/

Can't get a reset button to clear out a checkbox

I'm using Vue.js v2 and I've defined a single-file component, RegionFacet.vue. It lists some regions that relate to polygons on a map (click a value, the corresponding polygon appears on the map).
Separately, I have a reset button. When that gets clicked, I call a method in RegionFacet to unselect any checkboxes displayed by RegionFacet. The model does get updated, however, the checkboxes remain checked. What am I missing?
<template>
<div class="facet">
<div class="">
<div class="panel-group" id="accordion">
<div class="panel panel-default">
<div class="panel-heading">
<a data-toggle="collapse"v-bind:href="'#facet-' + this.id"><h4 class="panel-title">Regions</h4></a>
</div>
<div v-bind:id="'facet-' + id" class="panel-collapse collapse in">
<ul class="list-group">
<li v-for="feature in content.features" class="list-group-item">
<label>
<input type="checkbox" class="rChecker"
v-on:click="selectRegion"
v-bind:value="feature.properties.name"
v-model="selected"
/>
<span>{{feature.properties.name}}</span>
</label>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['content'],
data: function() {
return {
id: -1,
selected: []
}
},
methods: {
selectRegion: function(event) {
console.log('click: ' + event.target.checked);
if (event.target.checked) {
this.selected.push(event.target.value);
} else {
var index = this.selected.indexOf(event.target.value);
this.selected.splice(index, 1);
}
this.$emit('selection', event.target.value, event.target.checked);
},
reset: function() {
this.selected.length = 0;
}
},
created: function() {
this.id = this._uid
}
}
</script>
<style>
</style>
You are directly setting the array length to be zero, which cannot be detected by Vue, as explained here: https://v2.vuejs.org/v2/guide/list.html#Caveats
Some more info: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
To overcome this, you may instead set the value of this.selected as follows:
reset: function() {
this.selected = [];
}