Vuetify pagination not cycling through on button click - vue.js

I am having trouble implementing Veutify's pagination component to cycle through my array.
<template>
<div class="grid grid-cols-4 gap-5">
<div v-for="(item, index) in pagedAssets" :key="`asset_index_${index}`">
{{ item }}
</div>
<v-pagination
v-model="pageNo"
:length="numPages"
></v-pagination>
</div>
</template>
<script setup>
let pageSize = 4;
let pageNo = 1;
let assets = ["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16"];
const numPages = computed(() => {
// calculate the number of pages we have
return Math.ceil(assets.length / pageSize);
});
const pagedAssets = computed(() => {
// get the start index for your paged result set.
// The page number starts at 1 so the active item in the pagination is displayed properly.
// However for our calculation the page number must start at (n-1)
const startIndex = (pageNo - 1) * pageSize;
// create a copy of your assets list so we don't modify the original data set
const data = [...assets];
// only return the data for the current page using splice
return data.splice(startIndex, pageSize);
});
</script>
I am expecting when cycling through that the items will update.

You need pageNo to be reactive, you can achieve that by making it a ref instead. You can read more about it here: https://vuejs.org/guide/essentials/reactivity-fundamentals.html
Here is a working example for your specific case: Playground

pageNo is a v-model variable so it should be reactive but you are using it as a static variable that's why it is not updating. you need to use ref to make it reactive.
Replace your script with this-
<script setup>
import { computed, ref } from "vue";
let pageSize = 4;
let pageNo = ref(1);
let assets = ["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16"];
const numPages = computed(() => {
// calculate the number of pages we have
return Math.ceil(assets.length / pageSize);
});
const pagedAssets = computed(() => {
// get the start index for your paged result set.
// The page number starts at 1 so the active item in the pagination is displayed properly.
// However for our calculation the page number must start at (n-1)
const startIndex = (pageNo.value - 1) * pageSize;
// create a copy of your assets list so we don't modify the original data set
const data = [...assets];
// only return the data for the current page using splice
return data.splice(startIndex, pageSize);
});
</script>

Related

Vue.js 3 Pinia store is only partially reactive. Why?

I'm using Pinia as Store for my Vue 3 application. The problem is that the store reacts on some changes, but ignores others.
The store looks like that:
state: () => {
return {
roles: [],
currentRole: 'Administrator',
elements: []
}
},
getters: {
getElementsForCurrentRole: (state) => {
let role = state.roles.find((role) => role.label == state.currentRole);
if (role) {
return role.permissions.elements;
}
}
},
In the template file, I communicate with the store like this:
<template>
<div>
<draggable
v-model="getElementsForCurrentRole"
group="elements"
#end="onDragEnd"
item-key="name">
<template #item="{element}">
<n-card :title="formatElementName(element.name)" size="small" header-style="{titleFontSizeSmall: 8px}" hoverable>
<n-switch v-model:value="element.active" size="small" />
</n-card>
</template>
</draggable>
</div>
</template>
<script setup>
import { NCard, NSwitch } from 'naive-ui';
import draggable from 'vuedraggable'
import { usePermissionsStore } from '#/stores/permissions';
import { storeToRefs } from 'pinia';
const props = defineProps({
selectedRole: {
type: String
}
})
const permissionsStore = usePermissionsStore();
const { getElementsForCurrentRole, roles } = storeToRefs(permissionsStore);
const onDragEnd = () => {
permissionsStore.save();
}
const formatElementName = (element) => {
let title = element.charAt(0).toUpperCase() + element.slice(1);
title = title.replace('-', ' ');
title = title.split(' ');
if (title[1]) {
title = title[0] + ' ' + title[1].charAt(0).toUpperCase() + title[1].slice(1);
}
if (typeof title == 'object') {
return title[0];
}
return title;
}
</script>
My problem is the v-model="getElementsForCurrentRole". When making changes, for example changing the value for the switch, the store is reactive and the changes are made successfully. But: If I try to change the Array order by dragging, the store does not update the order. I'm confused, because the store reacts on other value changes, but not on the order change.
What can be the issue here? Do I something wrong?
-Edit- I see the following warning on drag: Write operation failed: computed value is readonly
Workaround
As workaround I work with the drag event and write the new index directly to the store variable. But...its just a workaround. I would really appreciate a cleaner solution.
Here is the workaround code:
onDrag = (event) => {
if (event && event.type == 'end') {
// Is Drag Event. Save the new order manually directly in the store
let current = permissionsStore.roles.find((role) => role.value == permissionsStore.currentRole);
var element = current.permissions.elements[event.oldIndex];
current.permissions.elements.splice(event.oldIndex, 1);
current.permissions.elements.splice(event.newIndex, 0, element);
}
}
You should put reactive value on v-model.
getElementsForCurrentRole is from getters, so it is treated as computed value.
Similar to toRefs() but specifically designed for Pinia stores so
methods and non reactive properties are completely ignored.
https://pinia.vuejs.org/api/modules/pinia.html#storetorefs
I think this should work for you.
// template
v-model="elementsForCurrentRole"
// script
const { getElementsForCurrentRole, roles } = storeToRefs(permissionsStore);
const elementsForCurrentRole = ref(getElementsForCurrentRole.value);

Vuejs v-for so laggy when infinite scrolling

I have this weird vuejs effect where when I am adding a new object data, the v-for re-renders all the object even if its already rendered.
I am implementing an infinite scroll like facebook.
The Code
To explain this code, I am fetching a new data from firebase and then push the data into the data object when it reaches the bottom of the screen
var vueApp = new Vue({
el: '#root',
data: {
postList: [],
isOkaytoLoad: false,
isRoomPostEmpty: false,
},
mounted: function() {
// Everytime user scroll, call handleScroll() method
window.addEventListener('scroll', this.handleScroll);
},
methods: {
handleScroll: function()
{
var d = document.documentElement;
var offset = d.scrollTop + window.innerHeight;
var height = d.offsetHeight - 200;
// If the user is near the bottom and it's okay to load new data, get new data from firebase
if (this.isOkaytoLoad && offset >= height)
{
this.isOkaytoLoad = false;
(async()=>{
const lastPost = this.postList[this.postList.length - 1];
const room_id = PARAMETERS.get('room');
const q = query(collection(db, 'posts'), where('room', '==', room_id), orderBy("time", "desc"), limit(5), startAfter(lastPost));
const roomSnapshot = await getDocs(q);
roomSnapshot.forEach((doc) => {
const postID = doc.id;
(async()=>{
// Put the new data at the postList object
this.postList = [...this.postList, doc];
const q = query(collection(db, 'comments'), where('post_id', '==', postID));
const commentSnapshot = await getDocs(q);
doc['commentCount'] = commentSnapshot.size;
//this.postList.push(doc);
console.log(this.postList);
setTimeout(()=>{ this.isOkaytoLoad = true }, 1000);
})();
});
})();
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div v-if="postList.length > 0" class="card-containers">
<!-- I have a component `Postcard` in my js file and it works well -->
<Postcard
v-for="post in postList"
:key="post.id"
:post-id="post.id"
:owner-name="post.data().owner_displayName"
:owner-uid="post.data().owner_id"
:post-type="post.data().post_type"
:image-url="post.data().image_url"
:post-content="truncateString(linkify(post.data().post_content))"
:room="post.data().room"
:time="post.data().time.toDate()"
:likers="post.data().likers"
:comment-count="post.commentCount"
:file-url="post.data().file_url"
:file-name="post.data().file_name"
:downloads="post.data().downloads">
</Postcard>
</div>
Now, the problem is here ...
Look at this screen record, FOCUS AT THE MOUSE, it's lagging and I can't even click on those buttons when vuejs is adding and loading the new data
Here is the code That I used
What I suspect
I am suspecting that everytime I add a new data, the VueJS re-renders it all, which does that effect. How can I force vueJS to not re-render those data that is already rendered in the screen?
You've got two unnecessary async IIFE; the second one inside the forEach is particularly problematic because the async code inside it will be executed concurrently for each loop iteration, which has implications:
getDocs() will be fired all at once for each loop ieration, potentially spamming the server (assuming this is performing a network request). Was this your intention? It looks like you're only fetching at most 5 new posts, so this is probably OK.
The async function updates some state which will trigger Vue to re-render for each doc. This should be batched together at the end so Vue does as minimal updates as possible.
Also don't use var; use const or let instead. There's almost no good reason to use var anymore, let it die.
I can't say this will improve your performance substantially, but I recommend making the following changes (untested):
async handleScroll() {
const d = document.documentElement;
const offset = d.scrollTop + window.innerHeight;
const height = d.offsetHeight - 200;
// If the user is near the bottom and it's okay to load new data, get new data from firebase
if (this.isOkaytoLoad && offset >= height) {
// Prevent loading while we load more posts
this.isOkaytoLoad = false;
try {
// Get new posts
const lastPost = this.postList[this.postList.length - 1];
const room_id = PARAMETERS.get('room');
const q = query(collection(db, 'posts'), where('room', '==', room_id), orderBy("time", "desc"), limit(5), startAfter(lastPost));
const roomSnapshot = await getDocs(q);
// Fetch comments of each post. Do this all at once for each post.
// TODO: This can probably be optimized into a single query
// for all the posts, instead of one query per post.
await Promise.all(roomSnapshot.docs.map(async doc => {
const postID = doc.id;
const q = query(collection(db, 'comments'), where('post_id', '==', postID));
const commentSnapshot = await getDocs(q);
doc.commentCount = commentSnapshot.size;
}));
// Append the new posts to the list
this.postList.push(...roomSnapshot.docs);
} catch (ex) {
// TODO: Handle error
} finally {
// Wait a bit to re-enable loading
setTimeout(() => { this.isOkaytoLoad = true }, 1000);
}
}
}
Doing :post-content="truncateString(linkify(post.data().post_content))" in the template means linkify will be executed during each re-render. I suspect linkify may be potentially slow for long lists? Can this be pre-calculated for each post ahead of time?
You're registering a window scroll event listener when the component is mounted. If the component is ever destroyed, you need to unregister the event listener otherwise it'll still fire whenever the window is scrolled. This may not be an issue in your case, but for reusable components it must be done.

Mithril: how to use multiple root elements?

When using m.route, does everything under it have to be rendered using Hyperscript (not counting JSX)? Is it possible to mix pure HTML with multiple separate parts rendered using Hyperscript?
Suppose I have this HTML code:
<div id="root">
<div class="header">
<h3 id="header-text">Header Text</h3>
</div>
<div class="container">
<div id="content"></div>
<div id="sidebar"></div>
</div>
<div id="footer"></div>
</div>
Is it possible to use Mithril to render multiple parts of the HTML sections? Like so (pseudo code):
var root = document.getElementById('root');
var header = document.getElementById('header');
var content = document.getElementById('content');
var sidebar = document.getElementById('sidebar');
var footer = document.getElementById('footer');
var HeaderComponent = ...; // to render header text
var ContentComponent = ...; // main page content
var SidebarComponent = ...; // show links, bio, etc.
var Footer = ...; // contact info, etc.
var index = {
view: function(){
return [
{el: header, component: HeaderComponent},
{el: content, component: ContentComponent},
{el: sidebar, component: SidebarComponent},
{el: footer, component: FooterComponent}
];
}
};
m.route(root, '/', {
'/': index
});
so that multiple separate parts in the HTML code are rendered instead of just having a single root element? I really don't want to render the whole HTML skeleton template in Mithril as well, like this:
❌ DO NOT WANT:
// I DON'T want this
m('div#root', [
m('div.header', [
m('h3#header-text', m(HeaderComponent))
]),
m('div.container', [
m('div#content', m(ContentComponent)),
m('div#sidebar', m(SidebarComponent))
]),
m('div#footer', m(FooterComponent))
]);
I know it's manageable, but I'd really like the base skeleton template to be inside the .HTML file, so that I can seamlessly wrap more HTML tags later on, like using Bootstrap classes (cards, container, rows, extra wrapping divs needed, etc.). Thanks :)!
There's nothing stopping you from using the m.mount function several times, and several mount-points can be initialised this way:
var count = 0
m.mount(root1, {
view: () =>
m('button', {
onclick: () => count++,
textContent: 'Increase count',
}),
})
m.mount(root2, {
view: () =>
m('p', 'Count: ', count)
})
<h1>
Static page with multiple roots
</h1>
<div id=root1>
</div>
<p>
Intermediary static content
</p>
<div id=root2>
</div>
<script src="https://unpkg.com/mithril#2.0.4/mithril.min.js"></script>
Using #Barney's answer below, I managed to solve this using a mix of m.route for the main content and multiple m.mount's for the other components:
var header = document.getElementById('header');
var content = document.getElementById('content');
var sidebar = document.getElementById('sidebar');
var footer = document.getElementById('footer');
var HeaderComponent = ...; // to render header text
var SidebarComponent = ...; // show links, bio, etc.
var Footer = ...; // contact info, etc.
// this will be shown under content
var HomePage = {
view: function(){
return m('p', [
'Lorem ipusom dolor amit ',
m(m.route.Link, {href: '/about'}, 'About')
]);
}
};
var AboutPage = {
view: function(){
return m('p', 'This is the About page!');
}
};
//// MAIN SOLUTION ////
var Layout = {
// oninit is only run once
oninit: function(){
m.mount(header, HeaderComponent);
m.mount(sidebar, SidebarComponent);
m.mount(footer, FooterComponent);
},
// run on every route change:
view: function(vnode){
return m('div', vnode.children);
}
};
m.route(content, '/', {
// using RouteResolver
'/': {
render: function(){
return m(Layout, m(HomePage));
},
onmatch: function(){
// this will be called on route change, so update your mounted components as needed
HeaderComponent.title = "Home Page";
}
},
'/about': {
render: function(){
return m(Layout, m(AboutPage));
},
onmatch: function(){
HeaderComponent.title = "About Page";
}
}
});
m.route is only used for <div id="content">, while all the other separate components (header, sidebar, footer) are instantiated using m.mount. I've wrapped the router with a Layout component that's used for all the routes, as this allows me to init and mount the separate components that will remain when routes change and not be affected by the main router. The router had to be modified to use RouteResolver to allow for more control and flexbility.
Notes:
Only a single m.route allowed per application, but multiple m.mount's allowed
When the same component is assigned multiple routes (in this case, Layout), the subtree will NOT be deleted and rebuilt from ground up -- it will only be diffed and updated (source). This way, we can use Layout component's oninit lifecycle hook to mount our other separate components, because oninit will only be called once.
We must use a RouteResolver for the routes in m.route, which gives us more control over how the routes should be rendered and we also get hooks like onmatch, which fires BEFORE the route is initialized -- this is the perfect place to update our mounted components as needed!

Vue composition api - component doesnt render at start using reactive()

Just started using Vue composition-api (with Vue2) and I ran into some reactivity? problem.
In the template, I want to render list of elements based on selected date.
My template:
...
<div v-for="(act, idx) in elementsPerDay[dateMatchable]" :key="idx">
...some content...
</div>
...
Now the problem is that when the component is created and data are fetched, nothing is rendered and elementsPerDay are empty (in the template). Once I force template update (for example by calling vm.$forceUpdate()) everything works fine.
My setup:
...
const selectedDate = new Date()
const { data } = <async function to get data>
const dateMatchable = computed(() => formatDate(selectedDate, 'YYYY-MM-DD')) // returns selectedDate as YYYY-MM-DD
// Here I init my reactive object
const elementsPerDay = reactive({})
// Here I fill my elementsPerDay when data changes (basically grouping data by day)
watch(data, e => {
const elsPerDay = {}
for (let idx = 0; idx < data.length; idx++) {
const element = data[idx]
const elementDate = formatDate(data.datetime, 'YYYY-MM-DD')
if (elementsPerDay[elementDate] === undefined) {
elsPerDay[elementDate] = []
}
elsPerDay[elementDate].push(act)
})
// Assign data to my reactive object
Object.assign(elementsPerDay, elsPerDay)
})
...
return { ..., selectedDate, dateMatchable, elementsPerDay }
EDIT:
Using computed works:
const elementsPerDay = computed(() => {
if (!data.value) {
return {}
}
const elsPerDay = {}
for (let idx = 0; idx < data.value.length; idx++) {
const element = data.value[idx]
const elementDate = formatDate(element.datetime, 'YYYY-MM-DD')
if (elsPerDay[elementDate] === undefined) {
elsPerDay[elementDate] = []
}
elsPerDay[elementDate].push(element)
}
return elsPerDay
})
I realize that in Options API, you needed to $set() new fields in object for it to be reactive but I thought that reactive() somehow does this for me. Am i wrong?
Compoisition API plugin is not Vue 3. Actually, what you encounter is a limitation from Vue 2 : https://v2.vuejs.org/v2/guide/reactivity.html#For-Objects.
You need to use $set.
I found an issue about your problem on Github: https://github.com/vuejs/composition-api/issues/334.
If you're using https://github.com/vuejs/composition-api, you can use $set() by:
import { set } from '#vue/composition-api'
...
setup() {
set(object, 'key', 'value');
}

Vue rendering array of objects

I'm creating a basic app in vue that uses axios to make a get request to grab html data from a blog site and using the cheerio node package to scrape the site for elements such as blog title and the date posted of each blog articles. However, I'm having trouble trying to render the scraped elements into the html. Here's the code:
<template>
<div class="card">
<div
v-for="result in results"
:key="result.id"
class="card-body">
<h5 class="card-title">{{ result.title }}</h5>
<h6 class="card-subtitle mb-2 text-muted">{{ result.datePosted }}</h6>
</div>
</div>
</template>
<script>
const Vue = require('vue')
const axios = require('axios')
const cheerio = require('cheerio')
const URL = 'https://someblogsite.com'
export default {
data() {
return {
results: []
}
},
mounted: function() {
this.loadBlogs()
},
methods: {
loadBlogs: function() {
axios
.get(URL)
.then(({ data }) => {
const $ = cheerio.load(data)
let results = this
$('.post').each((i, element) => {
const title = $(element)
.children('.content-inner')
.children('.post-header')
.children('.post-title')
.children('a')
.text()
const datePosted = $(element)
.children('.content-inner')
.children('.post-header')
.children('.post-meta')
.children('.posted-on')
.children('a')
.children('.published')
.text()
this.results[i] = {
id: i + 1,
title: title,
datePosted: datePosted
}
})
})
.catch(console.error)
}
}
}
</script>
I tried declaring
let results = this
before the axios request to refer to the scope within export default, but still getting the indicator from VS Code that the scope is still within the loadBlogs function. Am I missing something? I greatly appreciate the help! Thanks!
I think your problem is that you're trying to set Property of an results array so Vue can't pick your data update. Instead you should construct new array from your parsed page and set it as this.results = newResultsArray:
loadBlogs: function() {
axios.get(URL).then(({data}) => {
const $ = cheerio.load(data)
const newResults = $('.post').map((i, element) => {
const title = $(element).children('.content-inner .post-header .post-title a').text()
const datePosted = $(element).children('.content-inner .post-header .post-meta .posted-on a .published').text()
return {
id: i + 1,
title: title,
datePosted: datePosted
}
})//.toArray() // this toArray call might be needed, I haven't worked with cheerio for some time and not sure whether it returns array or its own collection type like jQuery does
this.results = newResults;
}).catch(console.error)
}
Also it should be even simpler if you just use this.results.push({...}) instead of property assignment this.results[i] = {...} (but it is usually easier to handle whole arrays instead of inserting and removing parts of them, both are viable solutions in their respective use cases, though).
And please check out this documentation article about how Vue handles reactive updates, it describes the problem you've encountered.