I am trying to detect when a list has been fully populated with items so I can scroll to a specific one in React.
I've tried using setTimeout and requestAnimationFrame to delay the execution but both seemed hacky...
Use effect hook also doesn't work because it runs prior to the dom actually being repainted.
Heres what I have so far:
const observer = new MutationObserver(mutationList => {
console.log(mutationList);
if (listRef.current) {
if (listRef.current?.contains(searchedItemRef.current)) {
console.log('Found Node');
console.log(searchedItemRef.current);
searchedItemRef.current!.scrollIntoView({ block: 'center' });
}
}
});
useEffect(() => {
if (listRef.current)
observer.observe(listRef.current, {
childList: true,
subtree: true,
});
}, [props.activeChatChannel]); //some global state that represents chat data
Related
I am writing tests for Dark Mode Actions in cypress and I am operating mostly on header. Because of it I am catching it very often using cy.get("header). I am wondering if there is any way to save it in any variable so there is no need to catch it every time and use something like header.contains for example. Documentation of cypress says that simple const header = cy.get("header") doesn't work. Do you know any method to solve this problem so my code will be a little bit cleaner?
Part of test code
it("toggles darkmode", () => {
//when
cy.visit("localhost:3000");
cy.get("header").contains("title", "moon-icon").click({ force: true });
cy.get("header").should("contain", "sun-icon");
cy.get("header").contains("title", "sun-icon").click({ force: true });
cy.get("header").should("contain", "moon-icon");
});
it("remebers dark mode after refresh", () => {
//when
cy.visit("localhost:3000");
cy.get("header").contains("title", "moon-icon").click({ force: true });
cy.reload();
//then
cy.get("header").should("contain", "sun-icon");
});
Assuming all of your tests in this same describe block have the same setup, you could alias the cy.get('header') in a beforeEach.
describe('test', () => {
beforeEach(() => {
cy.visit('localhost:3000');
cy.get('header').as('header');
});
it("toggles darkmode", () => {
//when
cy.get("#header").contains("title", "moon-icon").click({ force: true });
cy.get("#header").should("contain", "sun-icon");
cy.get("#header").contains("title", "sun-icon").click({ force: true });
cy.get("#header").should("contain", "moon-icon");
});
});
Take a look at .within() to set scope of commands.
cy.get("header").within($header => {
cy.contains("title", "moon-icon")
.click()
.should("contain", "sun-icon")
cy.contains("title", "sun-icon")
.click()
.should("contain", "moon-icon")
})
I am trying to to use the PerformanceNavigationTiming API to generate a page load metric.
The MDN API document linked above says that the PerformanceEntry.duration should give me what I need because it:
[r]eturns a timestamp that is the difference between the PerformanceNavigationTiming.loadEventEnd and PerformanceEntry.startTime properties.
However, when I check this property, I get simply 0. I'm accessing this API from within a React hook that runs a useEffect function that wait for the window load event and then checks the api like so:
export const useReportPageLoadTime = () => {
useEffect(() => {
const reportTime = () => {
let navPerformance: PerformanceEntry
navPerformance = window.performance.getEntriesByType('navigation')[0]
console.log({
duration: navPerformance.duration,
blob: navPerformance.toJSON()
})
}
if (document.readyState === 'complete') {
reportTime()
return null
} else {
window.addEventListener('load', reportTime)
return () => window.removeEventListener('load', reportTime)
}
}, [])
}
As you can see there, I also call toJSON on the performance entry and indeed it shows that the values upon which duration (startTime and loadEventEnd) are both 0 as well:
Does anyone know why I am getting this value?
I was finally able to get this to work using a different method than the event listener. It certainly is logical that the data should be ready when the load event fires, but the only way I was able to get the data was to use another feature of the Performance API: the PerformanceObserver, which fires a callback when a new piece of data has become available.
Here is the code that worked for me:
export const useReportPageLoadMetrics = () => {
useEffect(() => {
const perfObserver = new PerformanceObserver((observedEntries) => {
const entry: PerformanceEntry =
observedEntries.getEntriesByType('navigation')[0]
console.log('pageload time: ', entry.duration)
})
perfObserver.observe({
type: 'navigation',
buffered: true
})
}, [])
}
So I have this nuxt page /pages/:id.
In there, I do load the page content with:
content: function(){
return this.$store.state.pages.find(p => p.id === this.$route.params.id)
},
subcontent: function() {
return this.content.subcontent;
}
But I also have an action in this page to delete it. When the user clicks this button, I need to:
call the server and update the state with the result
redirect to the index: /pages
// 1
const serverCall = async () => {
const remainingPages = await mutateApi({
name: 'deletePage',
params: {id}
});
this.$store.dispatch('applications/updateState', remainingPages)
}
// 2
const redirect = () => {
this.$router.push({
path: '/pages'
});
}
Those two actions happen concurrently and I can't orchestrate those correctly:
I get an error TypeError: Cannot read property 'subcontent' of undefined, which means that the page properties are recalculated before the redirect actually happens.
I tried:
await server call then redirect
set a beforeUpdate() in the component hooks to handle redirect if this.content is empty.
delay of 0ms the server call and redirecting first
subcontent: function() {
if (!this.content.subcontent) return redirect();
return this.content.subcontent;
}
None of those worked. In all cases the current page components are recalculated first.
What worked is:
redirect();
setTimeout(() => {
serverCall();
}, 1000);
But it is obviously ugly.
Can anyone help on this?
As you hinted, using a timeout is not a good practice since you don't know how long it will take for the page to be destroyed, and thus you don't know which event will be executed first by the javascript event loop.
A good practice would be to dynamically register a 'destroyed' hook to your page, like so:
methods: {
deletePage() {
this.$once('hook:destroyed', serverCall)
redirect()
},
},
Note: you can also use the 'beforeDestroy' hook and it should work equally fine.
This is the sequence of events occurring:
serverCall() dispatches an update, modifying $store.state.pages.
content (which depends on $store.state.pages) recomputes, but $route.params.id is equal to the ID of the page just deleted, so Array.prototype.find() returns undefined.
subcontent (which depends on content) recomputes, and dereferences the undefined.
One solution is to check for the undefined before dereferencing:
export default {
computed: {
content() {...},
subcontent() {
return this.content?.subcontent
👆
// OR
return this.content && this.content.subcontent
}
}
}
demo
I'm developing a real time chat app with Vue.js and Firebase realtime database.
When a new message is received, I want the chat window to scroll to the bottom. To achieve this, I created a watcher for the conversation data. However, the function in the watcher is executed before the DOM is updated so the scroll value isn't correct yet. Should I watch another property? How can I detect when the new data has been loaded into the DOM?
HTML Code
<div class="chat" ref="chat">
<div
v-for="(message,key) in conversations" :key="key">
<div class="message">
{{message.text}}
</div>
</div>
</div>
Script (I'm using VueFire)
const conversations = db.ref('conversations');
export default {
data() {
return {
conversations: {},
}
},
watch: {
conversations: function() {
//Scroll to bottom when new message received
this.$refs.chat.scrollTop = this.$refs.chat.scrollHeight;
}
}
}
I can fix this issue by setting a timeout but it's a dirty trick imo...
setTimeout(() => {
this.$refs.chat.scrollTop = this.$refs.chat.scrollHeight;
}, 300);
Thank you for your help.
Edit: DOMNodeInserted => MutationObserver
You could use a MutationObserver
Here is a working example: https://codepen.io/Qumez/pen/WNvOWwp
Create a scrollToBottom method:
...
methods: {
scrollToBottom() {
this.$refs.wrapper.scrollTop = this.$refs.wrapper.scrollHeight;
}
}
...
And call it whenever a new message is added:
...
data() {
return {
mo: {}
}
},
mounted() {
let vm = this;
this.mo = new MutationObserver((mutationList, observer) => {
vm.scrollToBottom();
});
this.mo.observe(this.$el, {childList: true})
}
...
My variable names are a bit different than yours, but it'll work in your code once you update it.
Trying to run a test for the following code, but node can't be found .Using jest and enzyme for ReactJS
render () {
return (
this.state.permissionsLoaded ?
this.state.localPermissions[globals.UI_DATASOURCEDESIGNER] ?
this.state.datasourcePermissionsLoaded ?
this.state.allowCurrentDatasource ?
<div>
<Modal isOpen={this.state.addRequestModalOpen} style={shareModal}>
<div title="Close Window Without Saving" className="sidemodal_addnew_x" onClick={() => {this.closeAddModal()}}><FontAwesome name='xbutton' className='fa-times' /></div>
Keep getting the following error: Method “simulate” is meant to be run on 1 node. 0 found instead.
Here is what I have so far for my test:
beforeEach(() => wrapper = mount(<MemoryRouter keyLength={0}><Datasource {...baseProps} /></MemoryRouter>));
it("Test Click event on Add DataSource ", () => {
wrapper.find('Datasource').setState({
permissionsLoaded:true,
localPermissions:true,
datasourcePermissionsLoaded:true,
allowCurrentDatasource:true,
addRequestModalOpen:true
})
wrapper.update();
wrapper.find('Datasource').find('.sidemodal_addnew_x').simulate('click')
});
Here as list of my state:
permissionsLoaded: false,
datasourcePermissionsLoaded: false,
allowCurrentDatasource: false,
localPermissions:{
[globals.UI_DATASOURCEDESIGNER]:false,
}
Well it looks like you are trying to find a node which will be conditionally rendered if all of the state variables you've mentioned are true, which none of them are (you are actually setting them all to false and updating the wrapper beforehand). This means that there is no .sidemodal_addnew_x to be found that can be used to simulate a click on, hence why you get that error message.
In case you've wanted to test for the existence of that component instead, you can do the following:
expect(wrapper.find('Datasource').find('.sidemodal_addnew_x').exists()).to.equal(false);
If you do want to test the click make sure the component gets .sidemodal_addnew_x gets rendered by settings the state variables to true:
it("Test Click event on Close Window Without Saving", (done) => {
baseProps.onClick.mockClear();
wrapper.find('Datasource').setState({
permissionsLoaded:true,
localPermissions:true,
datasourcePermissionsLoaded:true,
allowCurrentDatasource:true,
addRequestModalOpen:true,
}, () => {
wrapper.update();
wrapper.find('Datasource').find('.sidemodal_addnew_x').simulate('click');
done();
});
});