nextTick() not triggering DOM update - vue.js

I'm creating a messaging app and I'm having some trouble with scrolling to the bottom of an ion-content element when a new message is added to an array. I'm using the scrollToBottom() method that comes with ion-content, and I'm using the Composition API in Vue 3.
Consider this snippet:
setup(props) {
const replyContent = ref("")
const messages = ref([])
// References to ion-content in the template
const ionContent = ref(null)
const reply = async () => {
const message = await replyToThread(props.threadId, replyContent.value).then((message) => message)
messages.value.push(message)
nextTick(() => {
console.log("DOM updated!")
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
})
}
return { replyContent, messages, ionContent, reply }
}
replyToThread() performs an API call and returns the new message, and nextTick() should ensure me that the DOM has been updated so that I can have my way with it. The console does successfully log "DOM updated!", but no scrolling to the bottom happens.
But, and somehow this works every time nextTick() doesn't, when I replace the nextTick() code block with the following, it works flawlessly:
setTimeout(() => {
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
}, 200)
I have to set the timeout at around 200 ms, otherwise it doesn't work. But relying on this when something fancy like nextTick() should do the trick feels quite dirty. Does anyone know why this is happening?

That is because nextTick() only guarantees that the actual DOM has been updated: it doesn't mean that the browser has actually finished the layout of the page. That is the reason why you need an arbitrary timeout to ensure the scrolling works, because after 200ms the browser is likely to be done laying things out based on the updated DOM.
To fix this you will probably need to rely on window.requestAnimationFrame:
nextTick(() => {
window.requestAnimationFrame(() => {
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
});
});
If this feels like too much nesting for you, you can create a method that returns a promise based on rAF:
methods: {
rAF: function() {
return new Promise(r => window.requestAnimationFrame(r));
}
}
Then it's a matter of ensuring promises returned by both nextTick() and rAF() are resolved before scrolling:
await nextTick();
await this.rAF();
if (ionContent.value) {
ionContent.value.$el.scrollToBottom();
}

Related

Vue 2 / Nuxt 2 Emit From Axios With Dialog Confirm

i am using Vue 2 / nuxt to emit from a axios post* call which itself is called from a Buefy dialog confirm. The emit from this component will close the window / panel and then re-load the users.
If I call the axios request from the button, this works without any issues, but once being called from the dialog, it just don't work?
*most likely this will be updated to a delete request, just not gotten to that let
See code below:
removeUser() {
this.$buefy.dialog.confirm({
message: 'Continue on this task?',
onConfirm: () => {
this.removeUserFunc()
}
})
},
removeUserFunc() {
// console.log(that)
const that = this
// Build URL
const EndPoint = '/remove_user/' + this.id
this.$axios.post(EndPoint).then((res) => {
// User Remove Message
UserRemoved(this.$swal)
that.$parent.$emit('completed')
// console.log(this.$emit('complete'))
// // Emit 'completed' Message
console.log(that.$emit('completed'))
console.log(that)
}).catch((res) => {
console.log(res)
// Check For Errors
GeneralError(this.$swal)
})
}
I was thinking it was losing access to the correct this, so i was trying to pass that back in, but not sure that is the case?
I have also tried with await, while that sort of works? I think is firing the emit too fast, as it re-loads the users but it still includes the user that as just been deleted?
removeUser() {
this.$buefy.dialog.confirm({
message: 'Continue on this task?',
onConfirm: async() => {
this.removeUserFunc()
await this.$emit('completed')
}
})
},
The this keyword refers to the object the function belongs to, or the window object if the function belongs to no object.
Try to use .bind and use a ES5 function
removeUser() {
this.$buefy.dialog.confirm({
message: 'Continue on this task?',
onConfirm: function() {
this.removeUserFunc()
}.bind(this)
})
},

Vitest LitElement events

I have a storybook project where i am using Vite and LitElement components.
To test the components i thought i would use the Vitest library.
But i can't really test my components, it is like if the components aren't initialized / mounted / working (but they work fine in the stories., so i think the problem is with the testing).
I have a breadcrumb component, which dispatches a custom event on the connectedCallback function. On my story i can listen to this event, so i know it is being dispatched.
But i can seem to test it.
What i have:
on the breadcrumb component, inside the connectedCallback function
this.dispatchEvent(new Event('abc-breadcrumb-connected'));
on my breadcrumb.test.ts file:
import type { IWindow } from 'happy-dom';
import { expect, describe, it, beforeEach, vi } from 'vitest';
import '../abc-breadcrumb';
import { AbcBreadcrumb } from "../abc-breadcrumb";
declare global {
interface Window extends IWindow {}
}
describe('Abc breadcrumb', async () => {
it('Dispatches connected event', async () => {
const mockConnectedCallback = vi.fn(() => true);
window.addEventListener('abc-breadcrumb-connected', () => {
console.log('GOT THE EVENT');
mockConnectedCallback()
});
document.body.innerHTML = `
<abc-breadcrumb role="nav" aria-label="Breadcrumb" class="breadcrumb" ismobile="">
...
</abc-breadcrumb>
`;
await window.happyDOM.whenAsyncComplete();
await new Promise(resolve => setTimeout(resolve, 0));
expect(mockConnectedCallback).toHaveBeenCalled();
})
});
0n my vite.config.ts i have:
export default defineConfig({
test: {
globals: true,
environment: 'happy-dom',
},
...
})
the error i get:
AssertionError: expected "spy" to be called at least once
I have no idea why it isn't working an would be really happy to get some help.
Thanks!
At first look, using await new Promise(resolve => setTimeout(resolve, 0)); seems like the kind of thing I was unsuccessfully relying on myself to ensure enough ticks had passed for various operations to complete. This often worked in my local browser but failed in CI or resulted in flaky tests.
Why not set up the promise such that the event listener calls resolve() or possibly even mockConnectedCallback(). Then you can be certain the event isn't firing as opposed to only not having been fired when setTimeout resolves.
const mockConnectedCallback = vi.fn(() => true);
let connectedResolve;
const connectedPromise = new Promise(r => connectedResolve = r);
window.addEventListener('abc-breadcrumb-connected', () => {
console.log('GOT THE EVENT');
connectedResolve();
mockConnectedCallback();
});
document.body.innerHTML = `
<abc-breadcrumb role="nav" aria-label="Breadcrumb" class="breadcrumb" ismobile="">
...
</abc-breadcrumb>
`;
await window.happyDOM.whenAsyncComplete();
await connectedPromise;
expect(mockConnectedCallback).toHaveBeenCalled();
It makes the expect() a little redundant given that it won't be reached until the awaited promise resolves, but I think awaiting an explicit resolution makes things easier to reason about and doesn't hard-code various assumptions about microtask queues and things into the test code, which more often than not have come back to bite me later.

Vue.js component not loading/rendering data when called via URL or F5

I have a Vue.js SPA with some pages that display data from a backend. When I navigate the pages via the navbar, everything works fine, components and data are loaded.
When I'm on the page, e.g. localhost:8080/#/mypage and press F5, the data doesn't get loaded / rendered. Same goes for when I directly navigate to the page via the address bar.
The data gets loaded in this function:
async beforeMount() {
await this.initializeData();
}
I've tried to call the method in every lifecycle hook, i.e. created, beforeCreated, mounted etc...
In the mounted lifecycle hook I'm setting a boolean property to true, so that the table is only rendered when the component is loaded (done with v-if).
mounted() {
this.componentLoaded = true;
}
Not sure if this is important, but I've tried it with or without and it doesn't work.
I would really appreciate it if somebody knew whats happening here.
EDIT:
this.applications is a prop and contains multiple applications which contain instances. I want to add some variables from the backend to each application.
console.log(1) gets printed
console.log(2) does not
initializeData: function () {
let warn = 0;
console.log("1");
this.applications.forEach(async application => {
const instance = application.instances[0];
console.log("2");
let myData = null;
try {
const response = await instance.axios.get('url/myData');
myData = response.data;
} catch (err) {
}
let tmpCount = 0;
let tmpFulfilled = 0;
myData.forEach(ba => {
if(!ba.fulfilled){
warn++;
application.baAllFulfilled = false;
}else {
tmpFulfilled++;
}
tmpCount++;
})
console.log("3");
// Assign values
this.baTotalWarnings = warn;
application.baAnzahl = tmpCount;
application.baFulfilled = tmpFulfilled;
this.componentLoaded = true;
}
Try removing the async and await keywords from your beforeMount, and remove this.componentLoaded from mounted. Set it instead in the then block (or after await) in your initializeData method. I'm not sure Vue supports the async keyword in its lifecycle methods.
Something like this:
beforeMount() {
this.initializeData(); // start processing the method
}
methods: {
initializeData() {
callToBackend().then(() => {
this.componentLoaded = true // backend call ready, can now show the table
})
}
}

Which Lifecycle hook after axios get but before DOM render

I'm trying to render my DOM, dependent on some data I'm returning from an axios get. I can't seem to get the timing right. The get is in the created hook, but there is a delay between the get and actually receiving the data. Basically if there is info in seller_id then I need to show the cancel button, otherwise don't. Here is my code:
this is in my created hook
axios.get('https://bc-ship.c9users.io/return_credentials').then(response => {
this.seller_id = response.data.seller_id;
this.selected_marketplace = response.data.marketplace;
this.token = response.data.auth_token;
});
and then this is the logic to show or hide the button. I've tried created, mounted, beforeUpdate, and updated all with no luck. I've also tried $nextTick but I can't get the timing correct. This is what I have currently:
beforeUpdate: function () {
// this.$nextTick(function () {
function sellerIdNotBlank() {
var valid = this.seller_id == '';
return !valid;
}
if(sellerIdNotBlank()){
this.show_cancel_button = true;
}
// })
},
First, it is pointless to get your data from backend and try to sync with Vue.js lifecycle methods. It never works.
Also, you should avoid beforeUpdate lifecycle event. It is often a code smell. beforeUpdate is to be used only when you have some DOM manipulations done manually and you need to adjust them again before Vue.js attempt to re-render.
Further, show_cancel_button is a very good candidate for a computed property. Here is how component will look:
const componentOpts = {
data() {
return {
seller_id: '',
// ... some more fields
};
},
created() {
axios.get('https://bc-ship.c9users.io/return_credentials').then(response => {
this.seller_id = response.data.seller_id;
this.selected_marketplace = response.data.marketplace;
this.token = response.data.auth_token;
});
},
computed: {
show_cancel_button() {
return this.seller_id !== '';
}
}
}

Loading indicator for long (non-AJAX) function?

I have a function in my Vue app which takes some time (about 2-3 seconds) to complete. It is not an AJAX call.
I would like to include a loading indicator while this code executes, but I am having trouble accomplishing it. I thought I could do the following...
<div v-on:click="doThings()">{{stuff}}</div>
methods: {
doThings: function () {
this.loading = true
console.log(this.loading)
longFunction()
this.loading = false
console.log(this.loading)
}...
}
...but that doesn't work. doThings() seems to not execute anything until longFunction() is done. I even tried making a separate function and changing my button to perform two functions like this...
<div v-on:click="doLoading(); doThings();">{{stuff}}</div>
...but this is also doesn't work. Is what I'm trying to do possible?
Use async code for longFunction() and set this.loading to false after the Promise is resolved.
<div v-on:click="doThings()">{{stuff}}</div>
methods: {
doThings: function () {
this.loading = true
longFunction().then(() => {
this.loading = false
})
}
}
var longFunction = function() {
return new Promise((resolve, reject) => {
window.setTimeout(()=>{ // force a new (pseudo)thread
// do stuff, then
resolve()
},100); // ...some reasonably short interval. One millisecond is working for me when testing locally, but it might be better to include some extra buffer, to ensure Vue is in its next tick
});
}
Alternatively, you could pass an object reference to longFunction that your Vue component can watch for changes on, and use that as the signal back to the component that it can set loading to false.