I am learning to paginate data returned from an API using AXIOS. I have a working set of code, but there is a place in the code defined by bootstrap for :Total-rows, this is currently hardcoded but this creates extra rows based on the value rather than a computed value. I want to calculate the number of rows dynamically.
I know that I can count the response data from the api using: this.variable = response.data.length, but the way I am calling the data is using page variable to paginate.
Any suggestions on an efficient way to accomplish this somewhat seemingly simple call?
<template>
<div id="app">
<div class="row">
<div class="col-md-12">
<li v-for="item in todos" :key="item.id">
{{ item.name }} : {{ item.type }}
</li>
</div>
</div>
<b-pagination size="md" :total-rows="54" v-model="currentPage" :per-page="10" #input="getPostData(currentPage)">
</b-pagination>
</div>
</template>
VUE
<script>
//Import axios for REST API calls
import axios from 'axios'
import 'regenerator-runtime/runtime';
//Import bootstrap CSS
import 'bootstrap/dist/css/bootstrap.css'
//Import bootstrap vue CSS
import 'bootstrap-vue/dist/bootstrap-vue.css'
const baseURL = 'http://localhost:3000/todos?_page='+this.currentPage+'&_limit='+this.limit;
export default {
name: 'app',
data () {
return {
title: 'Vue.js Pagination Example With Bootstrap',
currentPage: 1,
limit: 5,
todos: [],
todoName: "",
todoType: "",
}
},
methods: {
// Fetches todos when the component is created.
getPostData (currentPage) {
axios.get('http://localhost:3000/todos?_page='+this.currentPage+'&_limit='+this.limit)
.then(response => {
//console.log(response)
// JSON responses are automatically parsed.
this.todos = response.data
})
.catch(e => {
this.errors.push(e)
})
},
async addTodo() {
const res = await axios.post(baseURL, {
name: this.todoName,
type: this.todoType,
});
this.todos = [...this.todos, res.data];
//resets the input field
this.todoName = "";
this.todoType = "";
},
}, //end of methods
//detects the current page on load
mounted(currentPage){
this.getPostData(currentPage)
}
}
</script>
You will need the API to return the total amount of rows, otherwise your frontend have no way of knowing how many pages to show.
You can find an example of this below, which use a dummy/testing API called reqres. This API returns various information, like the current page, total amount of rows and per page and of course the data for the requested page.
new Vue({
el: "#app",
data() {
return {
currentPage: 1,
totalRows: 0,
perPage: 0,
users: [],
request: null
}
},
methods: {
async getData(page) {
const response = await fetch(`https://reqres.in/api/users?page=${page}&per_page=3`).then(resp => resp.json())
this.perPage = response.per_page;
this.users = response.data;
this.totalRows = response.total;
// Only for testing purposes
this.request = response
}
},
created() {
this.getData(this.currentPage)
}
})
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/bootstrap#4.5.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-vue/2.18.1/bootstrap-vue.min.css" />
<script src="//cdn.jsdelivr.net/npm/vue#2.6.12/dist/vue.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-vue/2.18.1/bootstrap-vue.min.js"></script>
<div id="app">
<b-pagination
v-model="currentPage"
:total-rows="totalRows"
:per-page="perPage"
#change="getData">
</b-pagination>
<ul>
<li v-for="{ first_name, last_name } in users">
{{ first_name }} {{ last_name }}
</li>
</ul>
Request
<pre>{{ request }}</pre>
</div>
Related
The idea
I'm trying to build a display where activities are shown with some filters. The data comes from an API generated by the CMS. The filters are not shown in the code since its not relevant.
Problem
When manually defining the 'items' in the data property the v-for list rendering displays fine and gives the desired output. When pulling the data from the api and assigning them to items the v-for is not displaying anything.
My Thoughts
I think that the v-for is run before the api request is finished putting the data into the 'items' value. Currently I'm using 'created' property to fire the function, also used 'Mounted()' before, this also didn't work.
Versions Vue 2.6.14, Axios 0.21.1
Vue Code
var vue = new Vue({
el: '#app',
data: {
items: null,
},
created: function () {
this.fetchData()
},
methods: {
fetchData: function() {
axios
.get('/api/activities.json')
.then(response => (this.items = response.data.data))
}
}
})
Templating
<div id="app">
<ul class="example">
<li v-for="item in items">
{{ item }}
</li>
</ul>
</div>
Your code seems to be working just fine. Look below, I just replaced your api call with a dummy REST api call and it's working just fine. Please console out the data from response.data.data and see if you are really receiving an array there.
Vue.config.productionTip = false
Vue.config.devtools = false
let vue = new Vue({
el: '#app',
data: {
items: null,
},
created() {
this.fetchData()
},
methods: {
fetchData: function() {
axios.get('https://jsonplaceholder.typicode.com/users')
.then(response => { this.items = response.data })}
}
})
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div id="app">
<ul class="example">
<li v-for="item in items">
{{ item.id }} - {{ item.name }}
</li>
</ul>
</div>
</div>
I'm using vue-apollo to fetch data from backend and will bind the value into the components.
When the first assignment of the local data, vue will send the data immediately to the following components and generate the template.
But I will need to put an additional property to that object after that, but seems like the component won't generate new html although the data had changed.
new Vue({
el: '#app',
apolloProvider,
components: {
Category, Chart
},
data () {
return {
categories: null,
},
apollo: {
getDatafromBackend() {
...
...,
async result (result) {
this.categories = await result.data.entriesWithinCategories.categories;
this.total = result.data.entriesWithinCategories.total;
return this.categories = await this.categories.map((category, index) => {
category.color = randomColors[index];
return category;
});
}
},
})
v-bind in index.js
<Category v-for="category in categories" :key="category._id" :category="category" :total="total"></Category>
Category.vue
<template>
<div>
<h4 :style="{ color: category.color }" #click="toggle = !toggle">
{{ category.name }} - {{ category.percentage }}% {{ category.color }}
</h4>
</div>
</template>
It won't print the category.color and console.log shows the value is undefined.
My question is how do I trigger another binding when the data changed?
I am using:
Vue.js, vue-route, Vuetify, Firebase (database here) and vue-pdf
I am trying to load all pdf pages using "vue-pdf" but I get this error when I try to read the pdf.
Uncaught TypeError: Cannot read property 'novel' of undefined
I have an object named novel which is set to null initially. When I get the data from Firebase, it gets assigned to this novel object. I followed the tutorial here to load multiple pdf pages. However, in the tutorial a direct URL is used, but in my case I trying to access a specific value inside my object which is "this.novel.nove_url". I'm not sure what I got wrong here, my code is below. Any help is much appreciated.
<template>
<div>
<Navbar />
<v-container fluid>
<h1>{{novel.title}}</h1>
<div class="novel-display container">
<pdf
v-for="i in numPages"
:key="i"
:src="src" //this should display the loaded pdf file
:page="i"
style="display: inline-block; width: 100%"
#num-pages="pageCount = $event"
#page-loaded="currentPage = $event"
></pdf>
</div>
<div class="text-center">
<v-pagination v-model="currentPage" :length="pageCount" :total-visible="7" circle></v-pagination>
</div>
</v-container>
<Footer />
</div>
</template>
<script>
import Navbar from "./Navbar";
import Footer from "./Footer";
import pdf from "vue-pdf";
import db from "../db";
// var loadPDF = pdf.createLoadingTask(
// "https://cdn.filestackcontent.com/wcrjf9qPTCKXV3hMXDwK"
// );
var loadingTask = pdf.createLoadingTask(this.novel.novel_url); //the file I would like to load
export default {
name: "Read",
components: {
Navbar,
Footer,
pdf,
},
data() {
return {
novel: null, // data from Firebase is saved here
src: loadingTask ,
currentPage: 0,
pageCount: 0,
numPages: undefined,
};
},
mounted() {
this.src.promise.then((pdf) => {
this.numPages = pdf.numPages;
});
},
created() {
let ref = db
.collection("Novels")
.where("novel_slug", "==", this.$route.params.novel_slug);
ref.get().then((snapshot) => {
snapshot.forEach((doc) => {
this.novel = doc.data();
this.novel.id = doc.id;
});
});
},
};
</script>
You cannot use this outside of export default (see here). In addition, the Firebase database fetch is asynchronous, so the novel property is only populated when the promise returned by get() is fulfilled (i.e. in the callback functions passed to the then() method)
You can initialize loadingTask in the created() hook, in get().then(), as follows (untested):
import Footer from "./Footer";
import pdf from "vue-pdf";
import db from "../db";
export default {
name: "Read",
components: {
Navbar,
Footer,
pdf,
},
data() {
return {
novel: null, // data from Firebase is saved here
src: null ,
currentPage: 0,
pageCount: 0,
numPages: undefined,
};
},
mounted() {
this.src.promise.then((pdf) => {
this.numPages = pdf.numPages;
});
},
created() {
let ref = db
.collection("Novels")
.where("novel_slug", "==", this.$route.params.novel_slug);
ref.get().then((snapshot) => {
snapshot.forEach((doc) => {
this.novel = doc.data();
this.novel.id = doc.id;
});
this.src = pdf.createLoadingTask(this.novel.novel_url);
});
},
};
</script>
I had a page on which there was a header with an input that was a search engine, a list of posts, and pagination. I decided to move the header from this file to a separate component in a separate vue file. After I did this, the search for posts by title stopped working, and I can’t add a post now either. I think that I need to import my posts into a new file for my newly created component but how to do it.
My code when it worked(before my changes)
My code is not working after the changes:
The file in which my posts situated:
<template>
<div class="app">
<ul>
<li v-for="(post, index) in paginatedData" class="post" :key="index">
<router-link :to="{ name: 'detail', params: {id: post.id, title: post.title, body: post.body} }">
<img src="src/assets/nature.jpg">
<p class="boldText"> {{ post.title }}</p>
</router-link>
<p> {{ post.body }}</p>
</li>
</ul>
<div class="allpagination">
<button type="button" #click="page -=1" v-if="page > 0" class="prev"><<</button>
<div class="pagin">
<button class="item"
v-for="n in evenPosts"
:key="n.id"
v-bind:class="{'selected': current === n.id}"
#click="page=n-1">{{ n }} </button>
</div>
<button type="button" #click="page +=1" class="next" v-if="page < evenPosts-1">>></button>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Pagination',
data () {
return {
search: '',
current: null,
page: 0,
posts: [],
createTitle: '',
createBody: '',
visiblePostID: '',
}
},
watch: {
counter: function(newValue, oldValue) {
this.getData()
}
},
created(){
this.getData()
},
computed: {
evenPosts: function(posts){
return Math.ceil(this.posts.length/6);
},
paginatedData() {
const start = this.page * 6;
const end = start + 6;
return this.posts.filter((post) => {
return post.title.match(this.search);
}).slice(start, end);
},
},
methods: {
getData() {
axios.get(`https://jsonplaceholder.typicode.com/posts`).then(response => {
this.posts = response.data
})
},
}
}
</script>
Header vue:
AddPost
<script>
import axios from 'axios';
export default {
name: 'Pagination',
data () {
return {
search: '',
current: null,
posts: [],
createTitle: '',
createBody: '',
}
},
created(){
this.getData()
},
methods: {
getData() {
axios.get(`https://jsonplaceholder.typicode.com/posts`).then(response => {
this.posts = response.data
})
},
addPost() {
axios.post('http://jsonplaceholder.typicode.com/posts/', {
title: this.createTitle,
body: this.createBody
}).then((response) => {
this.posts.unshift(response.data)
})
},
}
}
</script>
App.vue:
<template>
<div id="app">
<header-self></header-self>
<router-view></router-view>
</div>
</template>
<script>
export default {
components: {
name: 'app',
}
}
</script>
You have a computed property paginatedData in your "posts" component that relies a variable this.search:
paginatedData () {
const start = this.page * 6;
const end = start + 6;
return this.posts.filter((post) => {
return post.title.match(this.search);
}).slice(start, end);
},
but this.search value is not updated in that component because you moved the search input that populates that value into the header component.
What you need to do now is make sure that the updated search value is passed into your "posts" component so that the paginatedData computed property detects the change and computes the new paginatedData value.
You're now encountering the need to pass values between components that may not have a parent/child relationship.
In your scenario, I would look at handling this need with some Simple State Management as described in the Vue docs.
Depending on the scale of you app it may be worth implementing Vuex for state management.
I'm working on a basic POC app that has a Solr search function on the front that finds products, and then links in the search results use a route to go to a Product detail page.
index.js
import Vue from 'vue'
import Router from 'vue-router'
import Search from '#/components/Search'
import Product from '#/components/Product'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/search',
name: 'Search',
component: Search
},
{
path: '/product_display/:language/:entity_id',
name: 'product_display',
component: Product
},
]
})
Product.vue
<template>
<section class="hero is-warning">
<div class="hero-body">
<div class="container">
<h1 class="title">
{{ product.ss_field_content_title }}
</h1>
</div>
</div>
<section class="section">
<div class="container">
<productSlideshow :slideshowNid="product.is_product_slideshow"
:language="product.ss_language"></productSlideshow>
</div>
</section>
</section>
</template>
<script>
import axios from 'axios'
import ProductSlideshow from '#/components/ProductSlideshow'
export default {
name: 'Product',
components: {
ProductSlideshow
},
data () {
return {
product: {}
}
},
created () {
this.getProduct()
},
methods: {
getProduct: function () {
const params = new URLSearchParams()
var entityId = this.$route.params.entity_id
var language = this.$route.params.language
params.append('fq', 'bundle:product_display')
params.append('fq', 'entity_id:' + entityId)
params.append('fq', 'ss_language:' + language)
params.append('wt', 'json')
params.append('rows', 1)
axios.get('https://my.solrurl.com/solr/indexname/select', {
params: params
})
.then(response => {
this.product = response.data.response.docs[0]
})
.catch(e => {
this.errors.push(e)
})
}
},
watch: {
'$route': 'getProduct'
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
Getting to this page works fine. The problem is getting the necessary params to the child ProductSlideshow component.
<template>
<h1>{{ slideshow.label }}</h1>
</template>
<script>
import axios from 'axios'
export default {
name: 'ProductSlideshow',
props: {
slideshowNid: {
type: Number,
required: true
},
language: {
type: String,
required: true
}
},
data () {
return {
slideshow: {},
slides: {}
}
},
created () {
this.getSlideshow()
},
methods: {
getSlideshow: function () {
var language = this.language
// Get slideshow record from Solr.
const params = new URLSearchParams()
params.append('fq', 'bundle:slideshow')
params.append('fq', 'entity_id:' + this.slideshowNid)
params.append('fq', 'ss_language:' + language)
params.append('fq', 'entity_type:node')
params.append('wt', 'json')
params.append('rows', 1)
axios.get('https://my.solrurl.com/solr/indexname/select', {
params: params
})
.then(response => {
this.slideshow = response.data.response.docs[0]
this.slides = this.slideshow.sm_field_slideshow_home
})
}
},
watch: {
'$route': 'getSlideshow'
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
What I'm seeing is no data returned from the Solr query for the slideshow record, and when inspecting the Solr query, it was because language was undefined in the query, so nothing could be returned. By using various debugger breakpoints, I figured out that what's happening is that getSlideshow() is being called before the results are returned from getProduct() in the Product component. I tried removing the created() method in the ProductDisplay component, but that didn't make a difference.
What do I need to change so that my getSlideshow() method in the ProductSlideshow component doesn't get called until the query from the Product component has been completed?
Your issue connected with the fact, that Vue fires created hooks in sync way, so all children templates are created and mounted before the response from API comes.
The solution is to use Conditional Rendering
In your Product.vue component create some Boolean value like isProductFetched and render the child only when this value will be true.
<template>
<section class="hero is-warning">
<div class="hero-body">
<div class="container">
<h1 class="title">
{{ product.ss_field_content_title }}
</h1>
</div>
</div>
<section class="section">
<div class="container">
<productSlideshow
v-if="isProductFetched"
:slideshowNid="product.is_product_slideshow"
:language="product.ss_language"></productSlideshow>
</div>
</section>
</section>
</template>
<script>
import axios from 'axios'
import ProductSlideshow from '#/components/ProductSlideshow'
export default {
name: 'Product',
components: {
ProductSlideshow
},
data () {
return {
isProductFetched: false,
product: {}
}
},
created () {
this.getProduct()
},
methods: {
getProduct: function () {
const params = new URLSearchParams()
var entityId = this.$route.params.entity_id
var language = this.$route.params.language
params.append('fq', 'bundle:product_display')
params.append('fq', 'entity_id:' + entityId)
params.append('fq', 'ss_language:' + language)
params.append('wt', 'json')
params.append('rows', 1)
axios.get('https://my.solrurl.com/solr/indexname/select', {
params: params
})
.then(response => {
this.product = response.data.response.docs[0]
// At this point, the product is fetched
this.isProductFetched = true
})
.catch(e => {
this.errors.push(e)
})
}
},
watch: {
'$route': 'getProduct'
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
As a result, the child component won't be rendered until this value becomes true. Also, do not forget to handle the error case.