Laravel vue infinite scroll won't pull new data - javascript

I am using element UI and it has infinite scroll with it, but i can't get it to work in my app.
Code
script
data() {
return {
count: '', // default is 8
total: '', // defalt is 15
perPage: '', // default is 8
loading: false,
products: [],
}
},
computed: {
noMore () {
return this.count >= this.total // if this.count (8) reach this.total (15) then show no more text.
},
disabled () {
return this.loading || this.noMore
}
},
methods: {
load () {
this.loading = true
setTimeout(() => {
this.count += this.perPage // add 8 more to page
this.loading = false
}, 1000)
},
getProducts: function(){
axios.get('/api/products').then(response => {
this.products = response.data.data;
this.count = response.data.meta.per_page; // set count to 8 (backend based)
this.perPage = response.data.meta.per_page; // set perPage to 8 (backend based)
this.total = response.data.meta.total; // my testing data are 15 so it sets total to 15 (backend based)
}).catch((err) => console.error(err));
}
},
mounted() {
this.getProducts();
}
HTML
<div class="row mt-5 my-5 infinite-list" v-infinite-scroll="load" infinite-scroll-disabled="disabled"> // load function
<div class="col-md-3 mb-3" v-for="product in products" :key="product.slug" :offset="1"> // loop function
<el-card shadow="hover" :body-style="{ padding: '0px' }">
{{product.name}}
</el-card>
</div>
// infinite functions
<div class="col-md-12 mt-3" v-if="loading">
<el-card shadow="always" class="text-center">Loading...</el-card>
</div>
<div class="col-md-12 mt-3" v-if="noMore">
<el-card shadow="always" class="text-center">That's it, No more!</el-card>
</div>
</div>
Meta data
Result
What did I do wrong in my code?

its because of your load function take look on it,
load () {
this.loading = true
setTimeout(() => {
this.count += this.perPage // add 8 more to page
this.loading = false
}, 1000)
},
you have set time out for 1000 ms which is called after each scroll
first count is updated by fetch call and then second time its updated by time out call back of load function ,
instead of making this.loading =false after 1000 ms you should make it false on success of fetch call and same thing apply for updating this.count, hope this will help you.
EDIT:
your load function should be like following
load () {
this.loading = true
this.getProducts();//previously you getproduct is called once only after mounted,you you need to call it each time whenever load is called
},
and you fetch function should be like
getProducts: function(){
axios.get('/api/products').then(response => {
this.products = response.data.data;
this.count += this.perPage // add 8 more to page
this.loading = false
this.perPage = response.data.meta.per_page; // set perPage to 8 (backend based)
this.total = response.data.meta.total; // my testing data are 15 so it sets total to 15 (backend based)
}).catch((err) => console.error(err));
}
another thing you can try is ,use getProducts instead of load and remove load function, make this.loading=true in your getProducts function,
previously your getProducts is called only once when component is mounted.you need to call getProducts function each time whenever load more is requested.

Related

Running asynchronous setTimeout counter parallely to Promise.all() to display a progress loader in %

I am having an issue running an asynchronous setTimeout counter parallel to a Promise.all() handler when trying to display a progress loader in %.
Here the details:
I've built a Vue app consisting of three components.
The first component "Test.vue" is importing a progress bar component and a pivot component which is containing about 12 pivot tables with data from different Google firestore collections (handled via Promise.all()).
Currently, it takes about 15 seconds until the pivot component is rendered successfully.
During this waiting period, I want to show a progress bar in % which goes up to 100% until all data for the pivot component is loaded and rendered successfully.
I was trying different approaches and currently, I am following the "easy" approach that the progress bar just shall display within 15 seconds the loading progress.
But even this approach doesn't work properly for me.
It seems to me like the progress functionality always waits until the loading and rendering of the pivot component is finished.
I really don't have any idea anymore how to solve this.
Which recommendations do you have?
Hint (if important): Inside the pivot component the data is loaded inside the mounted-hook via Promise.all()
Here the code for the Test.vue component:
Test.vue:
<template>
<mdb-container class="full-width" style="margin-top:35px !important;">
<mdb-row v-if="tabChange">
<mdb-progress :height="30" :value="value">{{value}} %</mdb-progress>
</mdb-row>
<mdb-card>
<mdb-card-header>
<mdb-tab default class="card-header-tabs">
<mdb-tab-item
v-for="tab in tabs"
v-bind:key="tab"
v-bind:class="[{ active: currentTab === tab }]"
:active="currentTab == tab"
#click.native.prevent="currentTab=tab"
>{{ tab }}</mdb-tab-item>
</mdb-tab>
</mdb-card-header>
<mdb-card-body>
<mdb-tab-content>
<mdb-tab-pane class="fade">
<mdb-row>
<keep-alive>
<component v-bind:is="currentTabComponent" v-on:finished="setFinished" class="tab"></component>
</keep-alive>
</mdb-row>
</mdb-tab-pane>
</mdb-tab-content>
</mdb-card-body>
</mdb-card>
</mdb-container>
</template>
<script>
/* eslint-disable */
import { mdbProgress, mdbContainer, mdbRow, mdbCol, mdbBtn, mdbCard, mdbCardTitle, mdbCardText, mdbCardFooter, mdbCardBody, mdbCardHeader, mdbListGroup, mdbListGroupItem, mdbNavItem, mdbCardGroup, mdbIcon, mdbFooter, mdbTab, mdbTabItem, mdbTabContent, mdbTabPane } from 'mdbvue';
import { db } from '#/main'
import PivotTable from '#/components/flight-builder/PivotTable'
import DefaultFilters from '#/components/flight-builder/filters/DefaultFilters'
import MainTab from '#/components/flight-builder/ads-archive/MainTab'
import Channel from '#/components/flight-builder/ads-archive/Channel'
import Loader from '#/components/Loader'
//import excel from "vue-excel-export"
/*const Channel = () => ({
component: import('#/components/flight-builder/ads-archive/Channel'),
loading: LoadingComponent,
error: LoadingComponent,
delay: 200,
timeout: 3000
})*/
export default {
name: 'AdsArchivePage',
components: {
PivotTable,
mdbContainer,
mdbRow,
mdbCol,
mdbBtn,
mdbCard,
mdbCardTitle,
mdbCardText,
mdbCardFooter,
mdbCardBody,
mdbCardHeader,
mdbListGroup,
mdbListGroupItem,
mdbNavItem,
mdbCardGroup,
mdbIcon,
mdbFooter,
mdbTab,
mdbTabItem,
mdbTabContent,
mdbTabPane,
mdbProgress,
DefaultFilters,
MainTab,
Channel,
Loader
},
data: () => {
return {
active: 0,
currentTab: "Main Tab",
value: 0,
tabs: ["Main Tab", "Channel", "Flight", "AdType", "Creative", "Spot length"],
componentMatcher: {
"Main Tab": "",
"Channel": Channel,
"Flight": "",
"AdType": "",
"Creative": "",
"Spot length": ""
},
finishedLoading: false
}
},
methods: {
setFinished(finishedLoading) {
this.finishedLoading = finishedLoading
},
timeout(ms) { //pass a time in milliseconds to this function
return new Promise(resolve => setTimeout(resolve, ms));
},
async wait() {
let loadingTime = 15
this.value = 0
console.log("wait123")
for (let i = 0; i<=loadingTime; i++) {
//setTimeout(() => {this.value = Math.round((i / loadingTime)*100)}, 15000);
this.value = Math.round((i / loadingTime)*100)
this.$forceUpdate()
await this.timeout(1000)
//await sleep(1000);
}
}
},
computed: {
currentTabComponent: function() {
return this.componentMatcher[this.currentTab]
},
tabChange: function() {
if (this.prevTab != this.currentTab) {
this.wait()
return true
}
return false
}
}
}
</script>
During this waiting period I want to show a progress bar in % which
goes up to 100% until all data for the pivot component is loaded and
rendered successfully.
I can tell you that the #Keith's answer did the trick like below to show all promises progress:
const allProgress = (proms, progress_cb) => {
let d = 0;
progress_cb(0); // Start progress_cb
for (const p of proms) { // Interate all promises
p.then(() => {
d++;
progress_cb( (d * 100) / proms.length ); // Display each item when it's done.
});
}
return Promise.all(proms);
}
const test = ms => new Promise((resolve) => {setTimeout(() => resolve(), ms);});
allProgress([test(1000), test(2000), test(3000)],
p => console.log(`% Done = ${p.toFixed(2)}`));
As a result, you can adjust the progress_cb method based on your needs (Instead of just console.log)
Updated
The second solution is to use promiss.all. Basically, this one the same as naortor's answer but refactor code.
var count = 0;
const waitThenTrigger = p => p.then(val => { progress(++count); return val;});
const progress = count => console.log(`% Done = ${(count*100/promiseArray.length).toFixed(2)}`);
const createPromise = (ms, value) => new Promise((resolve) => {setTimeout(() => resolve(value), ms);});
var promiseArray = [
waitThenTrigger(createPromise(3000, "a")),
waitThenTrigger(createPromise(2000, "b")),
waitThenTrigger(createPromise(1000, "c"))];
Promise.all(promiseArray).then(values => console.log(values));

How to re-render component after update data in store

I have a problem with re-render my table component when data in my store is updated,
I have a simply table with v-for like below:
<tr v-for="d in getDatas" v-bind:key="d.id">
and buttons to change page:
<button class="fa fa-chevron-left" #click="previousPage"/>
<button class="fa fa-chevron-right" #click="nextPage"/>
I load data to my datas before i open my component where is this table, and i get this data from store like below:
computed: {
getDatas () {
return this.$store.getters.getDatas;
}
},
but how to update my store with data when i click on button nextPage or previousPage?
here is my methods:
methods: {
...mapActions(["fetchDatas"]), //this is my action
nextPage() {
this.currentPage += 1;
},
previousPage() {
if(this.currentPage != 1) {
this.currentPage -= 1;
}
},
and my action:
async fetchDatas({ commit },{ currentPage}) {
const data = await fetch(`someURL&perPage=${currentPage}`);
const response = await data.json();
commit('setData', response);
}
so, how to reload component and my store when i click on my next/previousPage?
You already have a computed property that automatically updates when your store updates. All you have to do is trigger the action in your methods for changing pages.
nextPage() {
this.currentPage += 1;
this.fetchDatas(this.currentPage);
},
previousPage() {
if(this.currentPage != 1) {
this.currentPage -= 1;
this.fetchDatas(this.currentPage);
}
},
And unless you have a compelling reason for requiring an Object parameter to the action, I would remove the curly braces entirely:
async fetchDatas({ commit }, currentPage) {
...
Otherwise you need to call it with this.fetchData({currentPage: this.currentPage}).
You can setup a watch on the currentPagevariable where the getDatas action would be fired again on the change of the current page after clicking the buttons.
A watch can be declared as so:
watch: {
currentPage: function (newValue) {
this.$store.dispatch('fetchDatas', this.currentPage)
},
}

React function loose state on second try

this code works properly on first and third try, there's always a step where it bugs and send no data to the db, it ruin my business logic every time, I tried with and without page reload got the same results
apiCall = e => {
e.preventDefault();
window.scrollTo(0, 0);
this.setState({ loading: true });
const { currentUser } = fire.auth();
let userEmail = currentUser.email;
let userUnderscore = userEmail.replace(/\./g, '_');
let intervalMS = this.state.interval * 60000;
let url = `https://url.com/api/v1/${userUnderscore}/${this.state.urlTask}?url=${this.state.urlTask}&width=${this.state.width}&interval=${intervalMS}&user=${userUnderscore}&running=true`;
if (this.state.interval >= 1 && this.state.urlTask !== "") {
fetch(url)
}
else {
this.setState({ addURLError: true, loading: false });
}
this.setState({ addURL: true, addURLError: false });
fire.database().ref(`/master/users/${userUnderscore}/setup/`)
.update({
running: true,
interval: this.state.interval,
url: this.state.urlTask,
});
setTimeout(
function () {
this.setState({ addURL: false, running: true });
window.location.reload();
}.bind(this), 2000
);
}
...
<Button color='black' onClick={this.apiCall} animated='vertical' type='submit'>
EDIT
The function and the db actually get updated, the issue is that on second try the state is empty even if the form hold values
I fixed the issue by reloading the app before any attempt of executing the function, that way there's always a fresh state to be used : window.location.reload();

VueJS / JS DOM Watch / Observer in a multi phase render scenario

Scenario:
I’m developing a Vue scroll component that wraps around a dynamic number of HTML sections and then dynamically builds out vertical page navigation allowing the user to scroll or jump to page locations onScroll.
Detail:
a. In my example my scroll component wraps 3 sections. All section id’s start with "js-page-section-{{index}}"
b. The objective is to get the list of section nodes (above) and then dynamically build out vertical page (nav) navigation based on the n number of nodes found in the query matching selector criteria. Therefore, three sections will result in three page section navigation items. All side navigation start with “js-side-nav-{{index}}>".
c. Once the side navigation is rendered I need to query all the navigation nodes in order to control classes, heights, display, opacity, etc. i.e document.querySelectorAll('*[id^="js-side-nav"]');
EDIT
Based on some research here are the options for my problem. Again my problem being 3 phase DOM state management i.e. STEP 1. Read all nodes equal to x, then STEP 2. Build Side Nav scroll based on n number of nodes in document, and then STEP 3. Read all nav nodes to sync with scroll of document nodes:
Create some sort of event system is $emit() && $on. In my opinion this gets messy very quickly and feels like a poor solution. I found myself quickly jumping to $root
Vuex. but that feels like an overkill
sync. Works but really that is for parent child property state management but that again requires $emit() && $on.
Promise. based service class. This seems like the right solution, but frankly it became a bit of pain managing multiple promises.
I attempted to use Vue $ref but frankly it seems better for managing state rather than multi stage DOM manipulation where a observer event approach is better.
The solution that seems to work is Vues $nextTick(). which seems to be similar to AngularJS $digest. In essence it is a . setTimeout(). type approach just pausing for next digest cycle. That said there is the scenario where the tick doesn’t sync the time requires so I built a throttle method. Below is the code update for what is worth.
The refactored watch with nextTick()
watch: {
'page.sections': {
handler(nodeList, oldNodeList){
if (this.isNodeList(nodeList) && _.size(nodeList) && this.sideNavActive) {
return this.$nextTick(this.sideNavInit);
}
},
deep: true
},
},
The REFACTORED Vue component
<template>
<div v-scroll="handleScroll">
<nav class="nav__wrapper" id="navbar-example">
<ul class="nav">
<li role="presentation"
:id="sideNavPrefix + '-' + (index + 1)"
v-for="(item, key,index) in page.sections">
<a :href="'#' + getAttribute(item,'id')">
<p class="nav__counter" v-text="('0' + (index + 1))"></p>
<h3 class="nav__title" v-text="getAttribute(item,'data-title')"></h3>
<p class="nav__body" v-text="getAttribute(item,'data-body')"></p>
</a>
</li>
</ul>
</nav>
<slot></slot>
</div>
</template>
<script>
import ScrollPageService from '../services/ScrollPageService.js';
const _S = "section", _N = "sidenavs";
export default {
name: "ScrollSection",
props: {
nodeId: {
type: String,
required: true
},
sideNavActive: {
type: Boolean,
default: true,
required: false
},
sideNavPrefix: {
type: String,
default: "js-side-nav",
required: false
},
sideNavClass: {
type: String,
default: "active",
required: false
},
sectionClass: {
type: String,
default: "inview",
required: false
}
},
directives: {
scroll: {
inserted: function (el, binding, vnode) {
let f = function(evt) {
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f);
}
};
window.addEventListener('scroll', f);
}
},
},
data: function () {
return {
scrollService: {},
page: {
sections: {},
sidenavs: {}
}
}
},
methods: {
getAttribute: function(element, key) {
return element.getAttribute(key);
},
updateViewPort: function() {
if (this.scrollService.isInCurrent(window.scrollY)) return;
[this.page.sections, this.page.sidenavs] = this.scrollService.updateNodeList(window.scrollY);
},
handleScroll: function(evt, el) {
if ( !(this.isScrollInstance()) ) {
return this.$nextTick(this.inViewportInit);
}
this.updateViewPort();
},
getNodeList: function(key) {
this.page[key] = this.scrollService.getNodeList(key);
},
isScrollInstance: function() {
return this.scrollService instanceof ScrollPageService;
},
sideNavInit: function() {
if (this.isScrollInstance() && this.scrollService.navInit(this.sideNavPrefix, this.sideNavClass)) this.getNodeList(_N);
},
inViewportInit: function() {
if (!(this.isScrollInstance()) && ((this.scrollService = new ScrollPageService(this.nodeId, this.sectionClass)) instanceof ScrollPageService)) this.getNodeList(_S);
},
isNodeList: function(nodes) {
return NodeList.prototype.isPrototypeOf(nodes);
},
},
watch: {
'page.sections': {
handler(nodeList, oldNodeList){
if (this.isNodeList(nodeList) && _.size(nodeList) && this.sideNavActive) {
return this.$nextTick(this.sideNavInit);
}
},
deep: true
},
},
mounted() {
return this.$nextTick(this.inViewportInit);
},
}
</script>
END EDIT
ORIGINAL POST
Problem & Question:
PROBLEM:
The query of sections and render of navs work fine. However, querying the nav elements fails as the DOM has not completed the render. Therefore, I’m forced to use a setTimeout() function. Even if I use a watch I’m still forced to use timeout.
QUESTION:
Is there a promise or observer in Vue or JS I can use to check to see when the DOM has finished rendering the nav elements so that I can then read them? Example in AngularJS we might use $observe
HTML EXAMPLE
<html>
<head></head>
<body>
<scroll-section>
<div id="js-page-section-1"
data-title="One"
data-body="One Body">
</div>
<div id="js-page-section-2"
data-title="Two"
data-body="Two Body">
</div>
<div id="js-page-section-3"
data-title="Three"
data-body="THree Body">
</div>
</scroll-section>
</body>
</html>
Vue Compenent
<template>
<div v-scroll="handleScroll">
<nav class="nav__wrapper" id="navbar-example">
<ul class="nav">
<li role="presentation"
:id="[idOfSideNav(key)]"
v-for="(item, key,index) in page.sections.items">
<a :href="getId(item)">
<p class="nav__counter">{{key}}</p>
<h3 class="nav__title" v-text="item.getAttribute('data-title')"></h3>
<p class="nav__body" v-text="item.getAttribute('data-body')"></p>
</a>
</li>
</ul>
</nav>
<slot></slot>
</div>
</template>
<script>
export default {
name: "ScrollSection",
directives: {
scroll: {
inserted: function (el, binding, vnode) {
let f = function(evt) {
_.forEach(vnode.context.page.sections.items, function (elem,k) {
if (window.scrollY >= elem.offsetTop && window.scrollY <= (elem.offsetTop + elem.offsetHeight)) {
if (!vnode.context.page.sections.items[k].classList.contains("in-viewport") ) {
vnode.context.page.sections.items[k].classList.add("in-viewport");
}
if (!vnode.context.page.sidenavs.items[k].classList.contains("active") ) {
vnode.context.page.sidenavs.items[k].classList.add("active");
}
} else {
if (elem.classList.contains("in-viewport") ) {
elem.classList.remove("in-viewport");
}
vnode.context.page.sidenavs.items[k].classList.remove("active");
}
});
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f);
}
};
window.addEventListener('scroll', f);
},
},
},
data: function () {
return {
page: {
sections: {},
sidenavs: {}
}
}
},
methods: {
handleScroll: function(evt, el) {
// Remove for brevity
},
idOfSideNav: function(key) {
return "js-side-nav-" + (key+1);
},
classOfSideNav: function(key) {
if (key==="0") {return "active"}
},
elementsOfSideNav:function() {
this.page.sidenavs = document.querySelectorAll('*[id^="js-side-nav"]');
},
elementsOfSections:function() {
this.page.sections = document.querySelectorAll('*[id^="page-section"]');
},
},
watch: {
'page.sections': function (val) {
if (_.has(val,'items') && _.size(val.items)) {
var self = this;
setTimeout(function(){
self.elementsOfSideNavs();
}, 300);
}
}
},
mounted() {
this.elementsOfSections();
},
}
</script>
I hope I can help you with what I'm going to post here. A friend of mine developed a function that we use in several places, and reading your question reminded me of it.
"Is there a promise or observer in Vue or JS I can use to check to see when the DOM has finished rendering the nav elements so that I can then read them?"
I thought about this function (source), here below. It takes a function (observe) and tries to satisfy it a number of times.
I believe you can use it at some point in component creation or page initialization; I admit that I didn't understand your scenario very well. However, some points of your question immediately made me think about this functionality. "...wait for something to happen and then make something else happen."
<> Credits to #Markkop the creator of that snippet/func =)
/**
* Waits for object existence using a function to retrieve its value.
*
* #param { function() : T } getValueFunction
* #param { number } [maxTries=10] - Number of tries before the error catch.
* #param { number } [timeInterval=200] - Time interval between the requests in milis.
* #returns { Promise.<T> } Promise of the checked value.
*/
export function waitForExistence(getValueFunction, maxTries = 10, timeInterval = 200) {
return new Promise((resolve, reject) => {
let tries = 0
const interval = setInterval(() => {
tries += 1
const value = getValueFunction()
if (value) {
clearInterval(interval)
return resolve(value)
}
if (tries >= maxTries) {
clearInterval(interval)
return reject(new Error(`Could not find any value using ${tries} tentatives`))
}
}, timeInterval)
})
}
Example
function getPotatoElement () {
return window.document.querySelector('#potato-scroller')
}
function hasPotatoElement () {
return Boolean(getPotatoElement())
}
// when something load
window.document.addEventListener('load', async () => {
// we try sometimes to check if our element exists
const has = await waitForExistence(hasPotatoElement)
if (has) {
// and if it exists, we do this
doThingThatNeedPotato()
}
// or you could use a promise chain
waitForExistence(hasPotatoElement)
.then(returnFromWaitedFunction => { /* hasPotatoElement */
if (has) {
doThingThatNeedPotato(getPotatoElement())
}
})
})

Debounced function calls delayed but all executed when wait timer ends

I am using debounce in order to implement a 'search as you type' field.
I am reading https://css-tricks.com/debouncing-throttling-explained-examples/ and from what I can understand the function should only be called a limited number of times.
In my case, the function calls are delayed, but executed all at once once the wait timer ends:
methods: {
searchOnType: function (currentPage, searchString) {
console.log(`Searching ${searchString}`)
var debounced = throttle(this.getMovies, 4000, {leading: false, trailing: true})
debounced('movies.json', currentPage, searchString)
},
getMovies: function (url, page, query) {
console.log(query)
this.loading = true
resourceService.getMovies(url, page, query).then((result) => {
this.items = result.movies
this.totalMovies = result.total
this.loading = false
})
},
the HTML (it's Vue.JS)
<input
type="text"
v-model="searchString"
class="form-control input-lg"
placeholder="search movie"
#keydown="searchOnType(currentPage, searchString)"
>
This is my console.log:
You're creating a throttled function every time you keydown (you should probably be using input instead by the way). You can do this instead I think...
methods: {
searchOnType: throttle((currentPage, searchString) => {
this.getMovies('movies.json', currentPage, searchString)
}, 1000, {leading: false, trailing: true})
}

Categories

Resources