Code in $(document).ready, sometimes works sometimes doesnt - javascript

I am trying to dynamically map data from one object to another in an $(document).ready() function, the problem is that it works whenever it wants, the console.logs throughout the code are sometimes working, sometimes not.
var newBtnsObj = {};
$(document).ready(() => {
robots.forEach((robot) => {
newBtnsObj[robot.Name] = {};
newBtnsObj[robot.Name].Buttons = {};
console.log(newBtnsObj);
Object.keys(robot.ConfigData).forEach((dataType) => {
if (dataType === DataTypes.IO) {
Object.values(robot.ConfigData[`${dataType}`]).forEach((IOData) => {
if (IOData.length > 0) {
IOData.forEach((s) => {
console.log("HI");
newBtnsObj[robot.Name].Buttons[s.HtmlID] = {
DataType: dataType,
ID: s.ID,
Val: null,
};
});
}
});
} else {
Object.values(robot.ConfigData[`${dataType}`]).forEach((data) => {
console.log("doublehi");
newBtnsObj[robot.Name].Buttons[data.HtmlID] = {
DataType: dataType,
ID: data.ID,
Val: null,
};
});
}
});
});
});
robots is an array that is generated inside a socket.io event whenever someone connects to the page, I am using NodeJS on the backend to emit the "config" data that is used in the creation of the object and it is the first thing that is emitted when someone opens the page. I am opening the page locally and hosting the page and the server locally.
socket.on("config", (config) => {
config.forEach((robot) => {
robots.push(new Robot(robot.Name, robot.configData, robot.Events));
});
The order in which I load the files in the HTML is first the socket file and then the file with the $(document).ready code.
The problem is that newBtnsObj is sometimes created, sometimes not, if I add any new code and refresh the page it works only on the first refresh and then it is again an empty object. But even that doesnt work everytime, I can't even reproduce it 100%.
Now just for completeness I have added the full code, but even if I remove the newBtnsObj creation and leave it only with the console.log("hi")'s I have the same issue, so the issue is not in the object creation itself (if I copy and paste the code in the browser console it works perfectly). So even if its left to this it still has the same issue:
$(document).ready(() => {
robots.forEach((robot) => {
Object.keys(robot.ConfigData).forEach((dataType) => {
if (dataType === DataTypes.IO) {
Object.values(robot.ConfigData[`${dataType}`]).forEach((IOData) => {
if (IOData.length > 0) {
IOData.forEach((s) => {
console.log("HI");
});
}
});
} else {
Object.values(robot.ConfigData[`${dataType}`]).forEach((data) => {
console.log("doublehi");
});
}
});
});
});
Additionally, I thought that the problem may be that, I am trying to use the robots array before it is created from the config received in the initial socket event, so I have tried placing the code within the event itself, so that it executes right after the event is received, and it worked. Obviously that is the issue, how can I solve it? Is the only way to keep the code in the socket event, and if I want to extract it, I have to make another internal event to let the code know that the robots array is ready?

It seems like you programmed yourself a race-condition there. So your jQuery is executed whenever the page is loaded, regardless if the robots object is there or not. But the generation of the object is related to connections and network timing too. So your script only runs fine, if the generation is faster as your page loading.
And now you might ask why it would not work, if you copy the code below the generation itself. That is a race-condition too. Code inside a jQuery ready state will only be executed after page load. If you paste the code inside another script, like the object generation, the ready state may be registered at a time, where the page was already loaded. So it will never be executed, because you registered it to late.
So what you need to do, is to ensure that the code is only executed after the generation is finished. Maybe by using a callback, an event or something else. But for now, you have two related scripts what run whenever they want and nobody waits for each other. ;)
By the way, just a hint: Using $(document).ready(() => {}) is not best practice and out-dated too. There is a shorthand for that. And you should use jQuery instead of $ for calling it, so your scripts will work in noConflict mode too. So insted use jQuery(($) => {}).

Related

Mutating the DOM in React

So usually there are really easy ways to avoid mutating your DOM directly from React but unfortunately I'm not in one of those situations right now. I'm using an API that then embeds a lot of content on to my page into a div with an id of dashboard.
I need to manipulate the content on the page and move certain elements around and things. Currently I add an event listener to an element that exists before I call the API, with useEffect() like so:
useEffect(() => {
/*
Some code here...
*/
const setupDashboard = (dashboard) => {
document.querySelector('#run').addEventListener('click', () => {
dashboard.send('dashboard:run')
console.log("Run clicked")
})
console.log(document.querySelector("[data-title='States']"))
}
LookerEmbedSDK.createDashboardWithId(114)
.appendTo('#dashboard')
.on('dashboard:run:start',
/*() => updateState('#dashboard-state', 'Running')*/
)
.on('dashboard:run:complete',
/*() => updateState('#dashboard-state', 'Done')*/
)
.build()
.connect()
.then(setupDashboard)
.catch((error) => {
console.error('An unexpected error occurred', error)
})
});
The piece of code that adds the event handler to the button works, but the next console statement returns null. So it seems like there should be a better way to manipulate the DOM especially since I'm going to completely change it with all my modifications. I'll need to use all sorts of query selectors, deletions, appending and more. What's the best practice for all that.
Edit: to be clear, after the page loads if I run that line in the console it gets the element
I found the answer to my particular situation. In my case, the second query selector doesn't work because the embedded element is in an iframe that represents a second document and is therefore inaccessible.
However, if you're working with react and you need a way to mutate the DOM might I suggest using
ReactDOM.shouldComponentUpdate = () => false
found in this article https://www.reddit.com/r/reactjs/comments/2riuwa/mutating_reacts_dom/

Global JavaScript Listener to block specific URL curl from background/child process

I have a JavaScript embedded in HTML using a sdk.js
This SDK is using a compiled wasm file (not possible to decompile it), that child is performing a curl to a specific domain.
I am looking for a solution to include a global URL listener in my HTML JavaScript
in order to block the curl to that specific domain.
Is this somehow possible? Does anybody has a clever idea to solve that issue?
according to mdn on FetchEvent and mdn on Abortcontroller.abort() you could do something like
const el = document.querySelector('#exampleId')
const controller = new AbortController()
el.addEventListener('fetch', function(event) {
if (event.request.url === "someUrl") {
controller.abort()
}
})
You will need to attach this event listener to the element that fires the request. if there are going to be multiple elements that do that then you can add the same class to each and use this code to add an event listener to each one:
const elsArray = document.getElementsByClassName('exampleClass')
const controller = new AbortController()
elsArray.forEach(el => {
el.addEventListener('fetch', function(event) {
if (event.request.url === "someUrl") {
controller.abort()
}
}
})
never done that myself so let me know how it goes

How do I use cypress with components that are prefetched or preloaded with webpack?

I'm using Cypress 7.7.0 (also tested on 8.0.0), and I'm running into an interesting race condition. I'm testing a page where one of the first interactions that Cypress does is click a button to open a modal. To keep bundle sizes small, I split the modal into its own prefetched webpack chunk. My Cypress test starts with cy.get('#modal-button').click() but this doesn't load the modal because the modal hasn't finished downloading/loading. It does nothing instead (doesn't even throw any errors to the console). In other words, Cypress interacts with the page too quickly. This was also reproduced with manual testing (I clicked on the button super fast after page load). I have tried setting the modal to be preloaded instead, but that didn't work either.
I am able to solve the problem by introducing more delay between page load and button interaction. For example, inserting any Cypress command (even a cy.wait(0)) before I click on the button fixes the solution. Cypress, however, is known for not needing to insert these brittle solutions. Is there a good way to get around this? I'd like to keep the modal in its own chunk.
FYI: I'm using Vue as my front end library and am using a simple defineAsyncComponent(() => import(/* webpackPrefetch: true */ './my-modal.vue')) to load the modal component. I figure that this problem is general to Cypress though.
There's nothing wrong with cy.wait(0).
All you are doing is handing control from the test to the next process in the JS queue, in this case it's the app's startup script which is presumably waiting to add the click handler to the button.
I recently found that this is also needed in a React hooks app to allow the hook to complete it's process. You will likely also come across that in Vue 3, since they have introduced a hook-like feature.
If you want to empirically test that the event handler has arrived, you can use the method given here (modified for click()) - When Can The Test Start?
let appHasStarted
function spyOnAddEventListener (win) {
const addListener = win.EventTarget.prototype.addEventListener
win.EventTarget.prototype.addEventListener = function (name) {
if (name === 'click') {
appHasStarted = true
win.EventTarget.prototype.addEventListener = addListener // restore original listener
}
return addListener.apply(this, arguments)
}
}
function waitForAppStart() {
return new Cypress.Promise((resolve, reject) => {
const isReady = () => {
if (appHasStarted) {
return resolve()
}
setTimeout(isReady, 0) // recheck "appHasStarted" variable
}
isReady()
})
}
it('greets', () => {
cy.visit('app.html', {
onBeforeLoad: spyOnAddEventListener
}).then(waitForAppStart)
cy.get('#modal-button').click()
})
But note setTimeout(isReady, 0) will probably just achieve the same as cy.wait(0) in your app, i.e you don't really need to poll for the event handler, you just need the app to take a breath.
It seems like your problem is that you're already rendering a button before the code backing it is loaded. As you noticed, this isn't only an issue for fast automated bots, but even a "regular" user.
In short, the solution is to not display the button early, but show a loading dialog instead. Cypress allows waiting for a DOM element to be visible with even a timeout option. This is more robust than a brittle random wait.
I ended up going with waiting for the network to be idle, although there were several options available to me.
The cypress function I used to do this was the following which was heavily influenced by this solution for waiting on the network:
Cypress.Commands.add('waitForIdleNetwork', () => {
const idleTimesInit = 3
let idleTimes = idleTimesInit
let resourcesLengthPrevious
cy.window().then(win =>
cy.waitUntil(() => {
const resourcesLoaded = win.performance.getEntriesByType('resource')
if (resourcesLoaded.length === resourcesLengthPrevious) {
idleTimes--
} else {
idleTimes = idleTimesInit
resourcesLengthPrevious = resourcesLoaded.length
}
return !idleTimes
})
)
})
Here are the pros and cons of the solution I went with:
pros: no need to increase bundle size or modify client code when the user will likely never run into this problem
cons: technically still possible to have a race condition where the click event happens after the assets were downloaded, but before they could all execute and render their contents, but very unlikely, not as efficient as waiting on the UI itself for indication of when it is ready
This was the way I chose solve it but the following solutions would have also worked:
creating lightweight placeholder components to take the place of asychronous components while they download and having cypress wait for the actual component to render (e.g. a default modal that just has a spinner being shown while the actual modal is downloaded in the background)
pros: don't have to wait on network resources, avoids all race conditions if implemented properly
cons: have to create a component the user may never see, increases bundle size
"sleeping" an arbitrary amount (although this is brittle) with cy.wait(...)
pros: easy to implement
cons: brittle, not recommended to use this directly by Cypress, will cause linter problems if using eslint-plugin-cypress (you can disable eslint on the line that you use this on, but it "feels ugly" to me (no hate on anyone who programs that way)

Run window.addEventListener('load' ...) only once

I am wondering if there is any way to run window.addEventListener('load' ...) only the first time the specific page is loaded.
I tried just setting a flag called loaded to false, and only run the code inside the eventListener if loaded === false. Then once it's run, I set loaded to true. But does not work, still runs every time.
Can I perhaprs remove the eventListener once its run?
Keep a localStorage item that contains an array corresponding to all pages that have been loaded so far. Only attach the listener if that page isn't stored in localStorage yet. For example:
const { href } = window.location;
const alreadyLoaded = JSON.parse(localStorage.loaded || '[]');
if (!alreadyLoaded.includes(href)) {
alreadyLoaded.push(href);
localStorage.loaded = JSON.stringify(alreadyLoaded);
window.addEventListener('load', () => {
// rest of your code
});
}
Set the once property to true and it will run only once (doesn't work with Internet explorer)
More information here
const once = {
once : true
};
window.addEventListener('load',callback, once);
Easy way: you can use web storage that is if it's supported. Something like:
if (!localStorage.getItem("listenerLoaded")) {
window.addEventListener('load'...)
localStorage.setItem("listenerLoaded", true);
}
A bit tedious work would be using:
2. cookie(still browser needs support etc).
3. ajax and hold session
No it is not possible a a new execution context will be created every time that page loads.
You can try something like localStorage to save the state.
LocalStorage API helps you to save data which can be accessed later.it is an Object which can be used to access the current origin's local storage space.
For more info visit:
https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
Simply set a value in local storage once listener gets loaded then read that value before adding it again.
if (!localStorage.getItem("isLoaded")) {
window.addEventListener('load' ...)
localStorage.setItem("isLoaded", true);
}
Using removeEventListener is a good option:
var callback = function(){
...
}
window.removeEventListener('load',callback);
window.addEventListener('load',callback);

JavaScript Events not adding to the DOM

As an exercise I have to do a little online bike reservation app. This app begins with a header which explains how to use the service. I wanted this tutorial be optional so I wrote a welcome message in HTML and if the user doesn't have a var in his cookies saying he doesn't want to see the tutorial again, the welcome message is replaced by a slider that displays the information.
To achieve that is fetch a JSON file with all the elements I need to build the slider (three divs : the left one with an arrow image inside, the central one where the explanations occur and the right one with another arrow). Furthermore I want to put "click" events on the arrows to display next or previous slide. However, when I do so, only the right arrow event works. I thought of a closure problem since it is the last element to be added to the DOM that keeps its event but tried many things without success. I also tried to add another event to the div that works ("keypress") but only the click seems to work. Can you look at my code give me an hint on what is going on?
Here is the init function of my controller:
init: function() {
var load = this.getCookie();
if(load[0] === ""){
viewHeader.clearCode();
var diapoData = ServiceModule.loadDiapoData("http://localhost/javascript-web-srv/data/diaporama.json");
diapoData.then(
(data) => {
// JSON conversion
modelDiapo.init(data);
// translation into html
controller.initElementHeader(modelDiapo.diapoElt[0]);
controller.hideTuto();
}, (error) => {
console.log('Promise rejected.');
console.log(error);
});
} else {
viewHeader.hideButton();
controller.relaunchTuto();
}
}
There is a closer look at my function translating the JSON elements into HTML and adding events if needed :
initElementHeader: function(data){
data.forEach(element => {
// Creation of the new html element
let newElement = new modelHeader(element);
// render the DOM
viewHeader.init(newElement);
});
}
NewElement is a class creating all I need to insert the HTML, viewHeader.init() updates the DOM with those elements and add events to them if needed.
init: function(objetElt){
// call the render
this.render(objetElt.parentElt, objetElt.element);
// add events
this.addEvent(objetElt);
},
Finally the addEvent function:
addEvent: function(objet){
if(objet.id === "image_fleche_gauche"){
let domEventElt = document.getElementById(objet.id);
domEventElt.addEventListener("click", function(){
// do things
});
}
else if(objet.id === "image_fleche_droite"){
let domEventElt = document.getElementById(objet.id);
domEventElt.addEventListener("click", function(){
// do stuff
});
};
},
I hope being clear enough about my problem. Thank You.
Ok, I found the problem, even if the element was actually created, it somehow stayed in the virtual DOM for some time, when the "getElementById" in "addEvent" was looking for it in the real DOM it didn't find the target and couldn't add the event. This problem didn't occur for the last element since there was nothing else buffering in the virtual DOM.
On conclusion I took out the function adding events out of the forEach loop and created another one after the DOM is updated to add my events.

Categories