I'm trying to implement an infinite scroll in my vue chrome extension. I have this code but the problem is that when the page bottom is reached, the ajax call to fetch new data is fired more than once. How i can fix?
mounted() {
this.$store.commit('preloader', false)
window.onscroll = () => {
if( window.scrollY + window.innerHeight >= document.body.scrollHeight ){
console.log('fired')
this.nextPageLoaded = false
this.nextPage()
}
}
},
methods: {
nextPage() {
console.log('Loading next page')
axios.get('https://localhost:8080/api/media')
.then( (response) => {
console.log(response.data)
this.$store.commit('profileMedia', response.data.media)
this.nextPageLoaded = true
})
}
}
I've tried by setting a variable nextPageLoad to true after data are loaded and on false when the scroll event reach the bottom but not work as expected. Any solution will be appreciaetd
Perhaps it's a typo but I don't see that you are actually using the nextPageLoaded property as a flag in the if statement.
It should be something like
mounted() {
this.$store.commit('preloader', false)
window.onscroll = () => {
if ( (window.scrollY + window.innerHeight >= document.body.scrollHeight)
&& !this.nextPageLoaded) {
console.log('fired')
this.nextPageLoaded = true
this.nextPage()
}
}
},
methods: {
nextPage() {
console.log('Loading next page')
axios.get('https://localhost:8080/api/media')
.then( (response) => {
console.log(response.data)
this.$store.commit('profileMedia', response.data.media)
this.nextPageLoaded = false
})
}
}
Also have in mind that I switched the values of the nextPageLoaded property assignments because I think it's more intuitive this way (assigns to true immediately after triggering the nextPage method, assigns to false after ending the AJAX call).
Related
Code :-
<template>
// html
</template>
<script>
import _ from "lodash";
data() {
return {
renderComponent: false,
};
},
watch: {
// when this property is true, want to stop calling scroll event with this.onScroll method
renderComponent(val) {
if(val === true) {
console.log("////////I am removing you");
window.removeEventListener('scroll', this.onScroll);
}
}
},
methods: {
onScroll() {
console.log("I am called////////");
let similarTickerHeading = this.$refs.similarTicker;
if(similarTickerHeading) {
let margin = similarTickerHeading.getBoundingClientRect().top;
let innerHeigth = window.innerHeight;
console.log("Window Screen", innerHeigth);
console.log("Component located", margin);
// when this condition is true, I want to stop listening for the scroll event with this (onScroll method)
if(margin - innerHeigth < 850) {
console.log("I should start loading the actual component");
this.renderComponent = true;
this.$vs.loading.close("#loader-example > .con-vs-loading");
// removing eventListener for scrolling with the onScroll Method
window.removeEventListener('scroll', this.onScroll);
}
}
}
},
mounted() {
this.renderComponent = false;
this.$vs.loading({
container: "#loader-example",
type: "point",
scale: 0.8,
});
this.$nextTick(function() {
window.addEventListener('scroll', _.throttle(this.onScroll,250));
this.onScroll();
})
},
beforeDestroy() {
window.removeEventListener('scroll', this.onScroll);
},
</script>
In the above code, I want to stop listening for the scroll event with onScroll method when my if block in onScroll method becomes true. But, still, the onScroll method gets called whenever I scroll even though when I tried to remove the eventListener. I even created a watcher to remove the eventListener, yet the method keeps on getting called on scroll.
How can I remove the scroll eventListener with onScroll method ?
UPDATE : If I remove throttling and cut out _.throttle, the scroll event does get removed. Due to the use of _.throttle, I cannot remove the scroll event listener.
The function reference passed to window.addEventListener() must be the same reference passed to window.removeEventListener(). In your case, there are two different references because you've wrapped one of them with _.throttle().
Solution
Cache the function reference passed to addEventListener() so that it could be used later for removeEventListener():
export default {
mounted() {
👇
this._scrollHandler = _.throttle(this.onScroll, 250)
this.$nextTick(() => { 👇
window.addEventListener('scroll', this._scrollHandler);
this.onScroll();
})
},
beforeDestroy() {
👇
window.removeEventListener('scroll', this._scrollHandler);
},
}
demo
backAction = () => {
if (this.props.navigation.isFocused() && !this.state.otpScreen) {
this.setState({ showLoginScreen: true });
return true;
} else if(this.state.showLoginScreen) {
this.props.navigation.dispatch(CommonActions.reset({
index: 0,
routes: [
{ name: 'Login' },
],
}))
return false;
}
};
the code above is for action that should be done by the hardware back press, at first it should show the one screen and on the second press it should exit the app, both otp screen and login screen are on the same component, I'm just hiding based on the condition ...
at the first time it is working as per the condition but for the second time it's not.
componentDidMount() {
this.focusListener = this.props.navigation.addListener('focus', () => {
this.setState({
mobile: '',
showLoginScreen: true,
});
});
this.getDataFromStorage();
AsyncStorage.setItem('countryCode', '+91');
BackHandler.addEventListener(
"hardwareBackPress",
this.backAction
);
}
componentWillUnmount() {
clearInterval(this.interval)
BackHandler.addEventListener('hardwareBackPress', () => { return false })
}
can anyone pls help me how to do this,thanks in Advance
I guess your problem is that you can't access the 'current' value of a state inside a listener.
More Details here
Try to use a Reference instead of a state
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();
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())
}
})
})
I have a function which calls a service.
submitPost(value:any)
{
this._adminLogin.postAdminLogin(this.adminLoginmodel).subscribe(
data => {
this.responseStatus = data;
if(this.responseStatus.status == 1)
{
localStorage.setItem('admin_id', this.responseStatus.detail.id);
this._flashMessagesService.show(this.responseStatus.message, { cssClass: 'alert-success', timeout: 5000 });
top.location.href = 'admin/dashboard';
}
else
{
this._flashMessagesService.show(this.responseStatus.message, { cssClass: 'alert-danger', timeout: 2000 });
}
},
err => {
console.log(err)
},
() => {}
);
this.status = true;
}
My concern is with this section of code:-
if(this.responseStatus.status == 1)
{
localStorage.setItem('admin_id', this.responseStatus.detail.id);
this._flashMessagesService.show(this.responseStatus.message, { cssClass: 'alert-success', timeout: 5000 });
top.location.href = 'admin/dashboard';
}
Is there any way by which the redirect action could take place after the flash message disappears after 5000 ms? Something like this:-
if(this.responseStatus.status == 1)
{
localStorage.setItem('admin_id', this.responseStatus.detail.id);
this._flashMessagesService.show(this.responseStatus.message, { cssClass: 'alert-success', timeout: {function(){ top.location.href = 'admin/dashboard'; }, 5000 });
}
The following code should navigate after the message disappears. Yout flashMessage will be displayed for 5000 ms and your navigate should happen after 7000ms
if(this.responseStatus.status == 1)
{
localStorage.setItem('admin_id', this.responseStatus.detail.id);
this._flashMessagesService.show(this.responseStatus.message, { cssClass: 'alert-success', timeout: 5000 });
setTimeout(()=>{
top.location.href = 'admin/dashboard';
},7000);
}
I would do this by having the _flashMessagesService.show() return an Observable.
In the _flashMessageService, something like this:
myObserver : Observer<any>
function show(){
// do stuff
return Observable.create(observer =>{
this.myObserver =observer;
});
}
When you're ready to resolve the Observable, you can do:
this.myObserver.next('Possibly some value here');
this.myObserver.complete();
In your invoking code
this._flashMessagesService.show()
.subscribe(result=> { top.location.href = 'admin/dashboard'});}
I have been working on something similar.
An alternate approach, perhaps simpler is to just use Observable.timer in your main component:
Observable.timer(5000).subscribe(result=> { top.location.href = 'admin/dashboard'});
I prefer the first way, because it is contingent on the flashMessage service deciding when the flash message goes away, not on a hard coded time value.