Vue data value not updating for in function use - javascript

So i'm trying to lazy load articles with infinite scrolling, with inertia-vue and axios, backend is laravel 6.0. Since I don't want to do unnecessary request i'm giving over the total amount of articles to the component. I'm also tracking the amount of already loaded articles in the component data.
props: {
article_count: Number,
},
data: function () {
return {
loaded: 0,
scrolledToBottom: false,
articles: [],
}
},
The articles that are loaded are all being put into articles, and scrolledToBottom keeps track if the user has scrolled to the bottom. I have already checked if this refers to the vue component and has the data properties, which it does.
methods: {
load: function (nextToLoad) {
for (let i = nextToLoad; i < nextToLoad + 10; i++){
if (this.loaded <= this.article_count){
this.$axios.get(route('api_single_article', i))
.then(response => {
if (response.status == 200 && response.data != null) {
console.log(this.loaded);
this.articles.push(response.data);
}
this.loaded++;
})
.catch(error => {
if (error.response.status != 404) {
console.log(error);
console.log(this.loaded);
this.loaded++;
}
});
}
}
console.log(this.loaded);
},
scroll: function () {
window.onscroll = () => {
let bottomOfWindow = document.documentElement.scrollTop + window.innerHeight === document.documentElement.offsetHeight;
if (bottomOfWindow) {
this.load(this.loaded + 1);
}
}
}
}
The weird part is, that if i log this.loaded in the response / error arrow function it always returns 0. If i do it outside the response it also returns 0, but if i log this the loaded property has the supposed value after the loop has ran through, despite it being logged every time the loop is run. I have already showed this to a friend and he also couldn't fix it. The behaviour just seems weird to me and i don't know how else i should do, since i'm very inexperienced with vue.
Thanks for the help.

Might be a concurrency thing. Remember that the axios call is asynchronous so the loop will continue before the responses have been received.
So it could be the many responses are received at the same time and that loaded is then still 0 at that point. Looks like this won't be the case. Apparently JavaScript will process callbacks one after the other.
I'm not sure how JavaScript handles concurrent increments though so I can't exactly explain how that plays into effect here.
From your implementation, it looks like you'd want to turn the load function into an asynchronous function (using the async) keyword and then use the await before the axios call to ensure that you have received a response before making another request.
For interest's sake, maybe have a look at your network tab and count how many requests are being made. I suspect there will be more requests made than this.article_count.

Related

what does this piece of code mean in Javascript

I came across the following code:
let timeoutHandler;
clearTimeout(timeoutHandler);
timeoutHandler = setTimeout(() => {...});
This is an overly simplification since the original code is contained in a Vue application as follow:
public handleMultiSelectInput(value): void {
if (value === "") {
return;
}
clearTimeout(this.inputTimeoutHandler);
this.inputTimeoutHandler = setTimeout(() => {
axios.get(`${this.endpoint}?filter[${this.filterName}]=${value}`)
.then(response => {
console.log(response);
})
}, 400);
}
Does this mean this is some kind of cheap-ass debounce function?
Could someone explain what this exactly means.
Yes, it is a debounce function, which is when we wait for some amount of time to pass after the last event before we actually run some function.
There are actually many different scenarios where we might want to debounce some event inside of a web application.
The one you posted above seems to handle a text input. So it's debouncing the input, meaning that instead of fetching that endpoint as soon as the user starts to enter some character into the input, it's going to wait until the user stops entering anything in that input. It appears it's going to wait 400 milliseconds and then execute the network request.
The code you posted is kind of hard to read and understand, but yes, that is the idea of it.
I would have extracted out the network request like so:
const fetchData = async searchTerm => {
const response = await axios.get(`${this.endpoint}?filter[${this.filterName}]=${value}`);
console.log(response.data);
}
let timeoutHandler;
const onInput = event => {
if (timeoutHandler) {
clearTimeout(timeoutHandler);
}
timeoutHandler = setTimeout(() => {
fetchData(event.target.value);
}, 400);
}
Granted I am just using vanilla JS and the above is inside a Vuejs application and I am not familiar with the API the user is reaching out to. Also, even what I offer above could be made a lot clearer by hiding some of its logic.

Trouble getting DOM update in Vue.js while waiting for promise to complete

I'm not super versed in JS promises though I generally know enough to be dangerous. I'm working on Vue Method that handles searching a large data object present in the this.data() - Normally when I make asynchronous requests via axios this same formatting works fine but in this case I have to manually create a promise to get the desired behavior. Here is a sample of the code:
async searchPresets() {
if (this.presetSearchLoading) {
return
}
this.presetSearchLoading = true; // shows spinner
this.presetSearchResults = []; // removes old results
this.selectedPresetImports = []; // removes old user sections from results
// Need the DOM to update here while waiting for promise to complete
// otherwise there is no "loading spinner" feedback to the user.
const results = await new Promise(resolve => {
let resultSet = [];
for (var x = 0; x < 10000; x++) {
console.log(x);
}
let searchResults = [];
// do search stuff
resolve(searchResults);
});
// stuff after promise
}
The thing is, the stuff after promise works correctly. It awaits the resolution before executing and receives the proper search result data as it should.
The problem is that the DOM does not update upon dispatching the promise so the UI just sits there.
Does anyone know what I'm doing wrong?
Try $nextTick():
Vue 2.1.0+:
const results = await this.$nextTick().then(() => {
let resultSet = []
for (var x = 0; x < 10000; x++) {
console.log(x)
}
let searchResults = []
// do search stuff
return searchResults
});
Any Vue:
const results = await new Promise(resolve => {
this.$nextTick(() => {
let resultSet = []
for (var x = 0; x < 10000; x++) {
console.log(x)
}
let searchResults = []
// do search stuff
resolve(searchResults)
})
})
So it turns out I kind of said it all when I said, "I'm not super versed in JS promises though I generally know enough to be dangerous.".
I appreciate the attempts to help me through this but it turns out that making something a promise does not inherently make it asyncronous. This was my mistake. The problem wasn't that Vue was not updating the DOM, the problem was that the promise code was executing synchronously and blocking - thus because execution never actually stopped to await, Vue had no perogative to update the DOM.
Once I wrapped my promise code in a setTimout(() => { /* all code here then: */ resolve(searchResults); }, 200); Everything started working. I guess the set time out allows the execution to stop long enough for vue to change the dom based on my previous data changes. The script still technically blocks the UI while it runs but at least my loader is spinning during this process which is good enough for what I'm doing here.
See: Are JavaScript Promise asynchronous?
Vue will look for data changes and collect them into an array to tell if the DOM needs to be rerendered afterward. This means that everything in Vue is event(data)-driven. Your function only defines behavior that has no data binding to the V-DOM. So the Vue engine will do nothing since nothing in their dependant data set has changed.
I see your Promise function is going to resolve the response to a variable "searchResults". If your DOM uses that variable, the Vue engine will collect the change after the Promise's done. You may put a property in "data()" and bind it to DOM.
For example:
<span v-for="(res, key) in searchResults" :key="key">
{{ res.name }}
</span>
...
<script>
export default {
...
data () {
return { searchResults: [] }
},
...
}
</script>

how to deal with object when while fetching data it is initially null and then it gets filled with data

I have been trying to extract some information from the props this way.
let roleType=this.props.user.data.permissions.map((val)=>{
console.log(val);
});
In the initial stage when the component is getting rendered data has nothing inside it so I get an error that it can't map over which is true.
How do I deal with these cases. If I console log the above props I see that data is initially empty and then it gets filled. However, the web app crashes because of this?
In order to avoid cases when you try to deal with such scenarios, you either intialize the data in the format you expect it to be or you provide conditional checks to it while using
let roleType=this.props.user && this.props.user.data && this.props.user.data.permissions && this.props.user.data.permissions.map((val)=>{
console.log(val);
});
or initialise it like
state = {
user: {
data: {
permissions: []
}
}
}
What other thing that you can do to avoid unexpected undefined scenarios is to use PropType validation
Component.propTypes = {
user: PropTypes.shape({
data: PropTypes.shape({
permissions: PropTypes.Array
})
})
}
One other improvement that you can do over the first method is to write a function that checks for nested data
function checkNested(obj, accessor) {
var args = accessor.split('.')
for (var i = 0; i < args.length; i++) {
if (!obj || !obj.hasOwnProperty(args[i])) {
return false;
}
obj = obj[args[i]];
}
return true;
}
and then you can use it like
let roleType= checkNested(this.props.user, 'data.permissions') && this.props.user.data.permissions.map((val)=>{
console.log(val);
});
P.S. You can also improve the above method to take into consideration
the array index
Simply check to see if the object has the required attributes you're looking for before mapping through it. If it doesn't use an empty array.
let roleType = this.props.user.data ? this.props.user.data.permissions.map((val) => {
console.log(val);
}) : [];
Look like you have to make an API call to get the data, so add that code for fetching the data in componentDidmount once the response came, set the data response in the state. So that component will render again with updated data.
Also, put a condition on the rendering section where going to use the state so that it won't be trying to access any property of a null value.
EDIT: Like other people stated, the question doesn't really tell how you receive the data. My answer is based on you getting the data on load time. This answer doesn't work if you get the data externally or through an async call.
You can wait for the DOM Content to be fully loaded before running the code.
document.addEventListener('DOMContentLoaded', function() {
let roleType=this.props.user.data.permissions.map((val)=>{
console.log(val);
});
}, false);
Another way to do this is by waiting for the page to be fully loaded. This includes images and styling.
window.onload = function(e){
let roleType=this.props.user.data.permissions.map((val)=>{
console.log(val);
});
}
The first one is quicker in running, but you might be better of if you need the images to be loaded before running the code.
Finally it could also be a problem with the order of your code and simply placing your code lower might also fix the problem.

How can I turn an observable into an observable of long polling observables which complete on a specific value?

I'm creating an interactive webpage with RxJs.
This is what I want to achieve:
I have an application that generates tokens. These tokens can be consumed by an external entity.
When a user creates a token, the page starts polling the webserver for its status (consumed or not). When the token is consumed, the page refreshes.
So, when the token is created, a request is sent to the server every 2 seconds asking whether the token is consumed yet.
I have an Observable of strings that represent my generatedTokens.
I actually already have a working implementation using the Rx.Scheduler.default class, which allows me to do things manually. However, I can't help but feel that there should be a much simpler, more elegant solution to this.
This is the current code:
class TokenStore {
constructor(tokenService, scheduler) {
// actual implementation omitted for clarity
this.generatedTokens = Rx.Observable.just(["token1", "token2"]);
this.consumedTokens = this.generatedTokens
.flatMap(token =>
Rx.Observable.create(function(observer) {
var notify = function() {
observer.onNext(token);
observer.onCompleted();
};
var poll = function() {
scheduler.scheduleWithRelative(2000, function() {
// tokenService.isTokenConsumed returns a promise that resolves with a boolean
tokenService.isTokenConsumed(token)
.then(isConsumed => isConsumed ? notify() : poll());
}
);
};
poll();
}));
}
}
Is there something like a "repeatUntil" method? I'm looking for an implementation that does the same thing as the code above, but looks more like this:
class TokenStore {
constructor(tokenService, scheduler) {
// actual implementation omitted for clarity
this.generatedTokens = Rx.Observable.just(["token1", "token2"]);
this.consumedTokens = this.generatedTokens
.flatMap(token =>
Rx.Observable.fromPromise(tokenService.isTokenConsumed(token))
.delay(2000, scheduler)
// is this possible?
.repeatUntil(isConsumed => isConsumed === true));
}
}
Funnily enough the answer struck me a few minutes after posting the question. I suppose rubberducking might not be so silly after all.
Anyway, the answer consisted of two parts:
repeatUntil can be achieved with a combination of repeat(), filter() and first()
fromPromise has some internal lazy cache mechanism which causes subsequent subscriptions to NOT fire a new AJAX request. Therefore I had to resort back to using Rx.Observable.create
The solution:
class TokenStore {
constructor(tokenService, scheduler) {
// actual implementation omitted for clarity
this.generatedTokens = Rx.Observable.just(["token1", "token2"]);
this.consumedTokens = this.generatedTokens
.flatMap(token =>
// must use defer otherwise it doesnt retrigger call upon subscription
Rx.Observable
.defer(() => tokenService.isTokenConsumed(token))
.delay(2000, scheduler)
.repeat()
.filter(isConsumed => isConsumed === true)
.first())
.share();
}
}
A minor sidenote: the "share()" ensures that both observables are hot, which avoids the scenario where every subscriber would cause ajax request to start firing.
class TokenSource {
constructor(tokenService, scheduler) {
this.generatedTokens = Rx.Observable.just(["token1", "token2"]).share();
this.consumedTokens = this.generatedTokens
.flatMap(token =>
Rx.Observable.interval(2000, scheduler)
.flatMap(Rx.Observable.defer(() =>
tokenService.isTokenConsumed(token)))
.first(isConsumed => isConsumed === true))
.share()
}
}
You can take advantage of two facts:
flatMap has an overload that takes an observable which will be resubscribed to every time a new event comes in
defer can take a method returning a promise. The method will be re-executed every subscription, which means you do not have to roll your own Promise->Observable conversion.

PhantomJS: Ensuring that the response object stays alive in server.listen(...)

I'm using server.listen(...) from PhantomJS. I realize that it is largely experimental and that it shouldn't be used in production. I'm using it for a simple screenshot-server that accepts generates screenshots for a URL; it's a toy project that I'm using to play around with PhantomJS. I've noticed an issue with long-running requests in particular, where the response object is unavailable. Here are the relevant snippets from my code:
var service = server.listen(8080, function (request, response) {
response.statusCode = 200;
if (loglevel === level.VERBOSE) {
log(request);
} else {
console.log("Incoming request with querystring:", request.url);
}
var params = parseQueryString(request.url);
if (params[screenshotOptions.ACTION] === action.SCREENSHOT) {
getScreenshot(params, function (screenshot) {
response.headers["success"] = screenshot.success; //<-- here is where I get the error that response.headers is unavailable. Execution pretty much stops at that point for that particular request.
response.headers["message"] = screenshot.message;
if (screenshot.success) {
response.write(screenshot.base64);
} else {
response.write("<html><body>There were errors!<br /><br />");
response.write(screenshot.message.replace(/\n/g, "<br />"));
response.write("</body></html>");
}
response.close();
});
} else {
response.write("<html><body><h1>Welcome to the screenshot server!</h1></body></html>")
response.close();
}
});
getScreenshot is an asynchronous method that uses the WebPage.open(...) function to open a webpage; this function is also asynchronous. So what seems to be happening is that when the callback that is passed in as an argument to getScreenshot is finally called, it appears that the response object has already been deleted. I basically end up with the following error from PhantomJS:
Error: cannot access member `headers' of deleted QObject
I believe this is because the request times out and so the connection is closed. The documentation mentions calling response.write("") at least once to ensure that the connection stays open. I tried calling response.write("") at the beginning of server.listen(...) and I even tried a pretty hacky solution where I used setInterval(...) to perform a response.write("") every 500 milliseconds (I even lowered it down to as little as 50). I also made sure to clear the interval once I was done. However, I still seem to get this issue.
Is this something that I'm just going to have to deal with until they make the webserver module more robust? Or is there a way around it?
I was able to figure this out. It appears that while loading certain pages with WebPage.open (for example http://fark.com and http://cnn.com) multiple onLoadFinished events are fired. This results in the callback in WebPage.open being called multiple times. So what happens is that when control comes back to the calling function, I've already closed the response and so the response object is no-longer valid. I fixed this by using creating a flag before the WebPage.open function is called. Inside the callback, I check the status of the flag to see if I've already encountered a previous onLoadFinished event. Once I am with whatever I have to do inside the WebPage.open callback, I update the flag to show that I've finished processing. This way spurious (at least in the context of my code) onLoadFinished events are no-longer serviced.
(Note that the following refers to PhantomJS 1.9.7 while the OP was likely referring to 1.6.1 or older.)
In the event that multiple onLoadFinished events are being fired, you can use page.open() instead of listening for onLoadFinished yourself. Using page.open() will wrap your handler in a private handler to ensure that your callback is only called once.
From the source:
definePageSignalHandler(page, handlers, "_onPageOpenFinished", "loadFinished");
page.open = function (url, arg1, arg2, arg3, arg4) {
var thisPage = this;
if (arguments.length === 1) {
this.openUrl(url, 'get', this.settings);
return;
}
else if (arguments.length === 2 && typeof arg1 === 'function') {
this._onPageOpenFinished = function() {
thisPage._onPageOpenFinished = null;
arg1.apply(thisPage, arguments);
}
this.openUrl(url, 'get', this.settings);
return;
}
// ... Truncated for brevity
This functionality is exactly the same as the other answer, exposed as part of the official API.

Categories

Resources