Promise inside a forEach and variable references - javascript

I'm trying to make a simple Calendar app using Ionic (Cordova). I want to have a list of calendar events and a button next to each one which allows the user to add the event to his/her calendar or to remove it if it is already there. Also, I have put a button to add all the events to the calendar, for ease of use.
My problem is with using a Promise inside a forEach. Let me start with the code, which is from a Cordova (Ionic) app:
addAllEvents() {
this.events.forEach((event: CalendarEvent, index, array) => {
Calendar.createEvent(event.title, event.location, event.notes, event.startDate, event.endDate).then(
(msg) => {
console.log(msg);
event.isInCalendar = true;
},
(err) => {
console.log(err);
}
);
});
}
What addAllEvents() should do is (upon a button being pressed on the page) add all of the CalendarEvents in the events array to the user's Calendar and update the event's isInCalendar property to true. isInCalendar is used to decide which button to show next to each event (Add or Remove):
<button *ngIf="!event.isInCalendar;" ion-button rounded (click)="addEventToCalendar(i);">Add to calendar</button>
<button *ngIf="event.isInCalendar;" ion-button color="danger" rounded (click)="removeEventFromCalendar(i);">Remove from calendar</button>
However, upon execution, addAllEvents() only changes the isInCalendar property of the last event! (judging by the fact that only the last button changes)
What I suspect is happening is that forEach binds event to each consecutive item in the events array and by the time the Promises returned by Calendar resolve, the event object references the last item in the events array, which means that the Promises are modifying only the last event.
Please help me resolve this confusion!
P.S.
The individual addEventToCalendar() and removeEventFromCalendar() functions are working correctly. Here is their code:
addEventToCalendar(index: number) {
let event = this.events[index];
Calendar.createEvent(event.title, event.location, event.notes, event.startDate, event.endDate).then(
(msg) => {
console.log(msg);
event.isInCalendar = true;
},
(err) => {
console.log(err);
}
);
}
removeEventFromCalendar(index: number) {
let event = this.events[index];
Calendar.deleteEvent(event.title, event.location, event.notes, event.startDate, event.endDate).then(
(msg) => {
console.log(msg);
event.isInCalendar = false;
},
(err) => {
console.log(err);
}
);
}

Related

Inject content script only once in a webpage

I am trying to inject content script on context menu click in an extension manifest version 3. I need to check if it is already injected or not. If it is not injected , inject the content script. This condition has to be satisfied. Can anyone help me with this?
We can use
ALREADY_INJECTED_FLAG
but this can be checked only in the content script, so this approach will not work as expected.
payload.js(content script)
function extract() {
htmlInnerText = document.documentElement.innerText;
url_exp = /[-a-zA-Z0-9#:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9#:%_\+.~#?&//=]*)?/gi;
regex = new RegExp(url_exp)
list_url = htmlInnerText.match(url_exp)
ip_exp = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/;
list_ip = htmlInnerText.match(ip_exp)
hash_exp = /\b[A-Fa-f0-9]{32}\b|\b[A-Fa-f0-9]{40}\b|\b[A-Fa-f0-9]{64}\b/g
list_hash = htmlInnerText.match(hash_exp)
chrome.storage.local.set({ list_url: list_url, list_ip: list_ip, list_hash: list_hash });
}
chrome.runtime.sendMessage( extract());
background.js
genericOnClick = async () => {
// Inject the payload.js script into the current tab after the backdround has loaded
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
chrome.scripting.executeScript({
target: { tabId: tabs[0].id },
files: ["payload.js"]
},() => chrome.runtime.lastError);
});
// Listen to messages from the payload.js script and create output.
chrome.runtime.onMessage.addListener(async (message) => {
chrome.storage.local.get("list_url", function (data) {
if (typeof data.list_url != "undefined") {
urls = data.list_url
}
});
chrome.storage.local.get("list_ip", function (data) {
if (typeof data.list_ip != "undefined") {
ips = data.list_ip
}
});
chrome.storage.local.get("list_hash", function (data) {
if (typeof data.list_hash != "undefined") {
hashes = data.list_hash;
}
});
if ( hashes.length>0 || urls.length>0 || ips.length>0 ){
chrome.windows.create({url: "output.html", type: "popup", height:1000, width:1000});
}
});
}
on my first context menu click I get the output html once. Second time
I click, I get the output html twice likewise.
This behavior is caused by a combination of two factors.
First factor
You're calling chrome.runtime.onMessage.addListener() inside genericOnClick(). So every time the user clicks the context menu item, the code adds a new onMessage listener. That wouldn't be a problem if you passed a named function to chrome.runtime.onMessage.addListener(), because a named function can only be registered once for an event.
function on_message(message, sender, sendResponse) {
console.log("bg.on_message");
sendResponse("from bg");
}
chrome.runtime.onMessage.addListener(on_message);
Second factor
But you're not registering a named function as the onMessage handler. You're registering an anonymous function. Every click on the context menu item creates and registers a new anonymous function. So after the Nth click on the context menu item, there will be N different onMessage handlers, and each one will open a new window.
Solution
Define the onMessage handler as a named function, as shown above.
Call chrome.runtime.onMessage.addListener() outside of a function.
You don't have to do both 1 and 2. Doing either will solve your problem. But I recommend doing both, because it's cleaner.

Socket.IO emits multiple times in a room (Using with React.js)

I have a react app that allows multiple users to join a room and then the server picks a user randomly and then send the username of the selected user to everyone in the room. But the problem is that the server emits the same event multiple times(the number of emits is equal to the number of users in the room).
This is the code from the server that emits the event:
socket.on("selectNextPlayer", roomid => {
const selectedIndex = Math.round(Math.random()*rooms[roomid].participants.length);
const selectedUsername = rooms[roomid].participants[selectedIndex];
io.to(roomid).emit("setSelectedPlayer",selectedUsername); // THIS EVENT
connectedUsers[selectedUsername].emit("myTurn");
})
This is the code in the react client that listens for the event:
const socket = socketIOClient("http://127.0.0.1:8080");
useEffect(() => {
if(isRoomOwner){
socket.emit("createNewRoom", username,roomid);
}else {
socket.emit("joinNewRoom", username,roomid);
}
//Get all participants
socket.on("allParticipants", (allparticipants, roomOwner) => {
setParticipants(allparticipants);
setOwner(roomOwner);
})
//Start/Stop Game
socket.on("gameStatusChanged", gameStatus => {
setGameStarted(gameStatus);
//Select Next Player
socket.emit("selectNextPlayer",roomid);
})
//set selected player
socket.on("setSelectedPlayer", selectedPlayer => { //HERE
setSelectedPlayer(selectedPlayer);
setChats(prevChats => ([...prevChats,`Next player is ${selectedPlayer}`]))
})
//On my turn show truth/dare buttons
socket.on("myTurn", () => {
setIsMyTurn(true);
})
},[]);
The problem is that the element Next player is ${selectedPlayer} is added multiple times to the chats array which is stored in a state.
I tried moving const socket = socketIOClient("http://127.0.0.1:8080"); to another file and importing it at the top using import {socket} from '../services/socket'; this didn't work.
Edit: (Thanks to #Tushar)
selectNextPlayer is getting called multiple times.
socket.on("gameStatusChanged", gameStatus => {
setGameStarted(gameStatus);
//Select Next Player
socket.emit("selectNextPlayer",roomid);
})
This function call emits the selectNextPlayer function. This function listens for the gameStatusChanged event and calls emits a new selectNextPlayer function.
socket.on("startGame", roomid => {
console.log("SELECT START GAME CALLED");
io.to(roomid).emit("gameStatusChanged", true);
})
This event emits the gameStatusChanged event but this is getting called only once. So why is selectNextPlayer getting called multiple times ?
The problem is that
socket.on("gameStatusChanged", gameStatus => {
setGameStarted(gameStatus);
//Select Next Player
socket.emit("selectNextPlayer",roomid);
})
is on the client-side so for every client the "selectNextPlayer" event is emmited. So to fix this move socket.emit("selectNextPlayer",roomid); this to somewhere else for me it was to move it to the same examct event handler which emiits the startGame event.
{
username == owner && !gameStarted &&
<button
onClick={() => {
socket.emit("startGame",roomid);
socket.emit("selectNextPlayer", roomid);
}}
>
Start Game
</button>
}

My Buttons are not working after using fetch api along with express.js

I have strange problem with buttons that are requesting for displaying templates on client page.
This is client side code. The main task of entire class is to just enable user to click button, send request and get response with HTML that has been rendered from handlebars template and just paste it in partiuclar place on client side. It works, but only once. After first click and displaying elements, I totally lose any interaction with those buttons. There is no request, and there is no even EventListener for clicking. I get no error. Completely there is no single reaction after clicking.
class Weapons {
constructor() {
this.buttons = document.querySelectorAll('.type')
}
async displayWeapon(path) {
const container = document.querySelector('.shop-container')
await fetch(`weapons/${path}`).then(response => response.json()).then(data => container.innerHTML += data);
}
chooseWeapon() {
this.buttons.forEach(btn => {
btn.addEventListener('click', (e) => {
console.log('click');
let weaponType = e.target.dataset.type
switch (weaponType) {
case 'pistols':
console.log('click');
return this.displayWeapon(weaponType)
case 'rifles':
console.log('click');
return this.displayWeapon(weaponType)
case 'grenades':
console.log('click');
return this.displayWeapon(weaponType)
case 'closerange':
console.log('click');
return this.displayWeapon(weaponType)
case 'rocketlauchner':
console.log('click');
return this.displayWeapon(weaponType)
}
})
})
}
}
document.addEventListener('DOMContentLoaded', function () {
const weapons = new Weapons();
weapons.chooseWeapon();
> When I invoke displayWeapon(path) here it also works, but immidiately
> after displaying html elements clicking on buttons again does not
> initiate any action.
})
Here is app.get function but I doubt it's source of problem.
app.get('/weapons/:id', (req, res) => {
console.log('req');
console.log(req.url);
let type = req.params.id;
res.render(type, function (err, html) {
res.json(html);
})
})
Ok. The answer is actually simple. In fetch function container.innerHTML += data. This line deletes my html with buttons, and the same time it deletes eventListeners. So I need just to modify my html.

Replaced timeout event fires twice or more (sometimes)

I have a section of content that allows a user to edit it upon double-clicking on it. If the user changes the content and then stops for 2 seconds, the updated content is sent to the server to be saved.
To do so, I have bound an input event listener to that section that starts a 2 seconds countdown, and if there is already a countdown, the former will be cancelled and a new one will start instead. At the end of the countdown an http POST request is sent to the server with the new data.
The problem is that sometimes at the end of the countdown I see 2 or more requests sent, as if a countdown was not cancelled before a new one was inserted, and I can't figure out why.
The code in question is as follows:
//this function is bound to a double-click event on an element
function makeEditable(elem, attr) {
//holder for the timeout promise
var toSaveTimeout = undefined;
elem.attr("contentEditable", "true");
elem.on("input", function () {
//if a countdown is already in place, cancel it
if(toSaveTimeout) {
//I am worried that sometimes this line is skipped from some reason
$timeout.cancel(toSaveTimeout);
}
toSaveTimeout = $timeout(function () {
//The following console line will sometimes appear twice in a row, only miliseconds apart
console.log("Sending a save. Time: " + Date.now());
$http({
url: "/",
method: "POST",
data: {
action: "edit_content",
section: attr.afeContentBox,
content: elem.html()
}
}).then(function (res) {
$rootScope.data = "Saved";
}, function (res) {
$rootScope.data = "Error while saving";
});
}, 2000);
});
//The following functions will stop the above behaviour if the user clicks anywhere else on the page
angular.element(document).on("click", function () {
unmakeEditable(elem);
angular.element(document).off("click");
elem.off("click");
});
elem.on("click", function (e) {
e.stopPropagation();
});
}
Turns out (with help from the commentators above) that the function makeEditable was called more than once.
Adding the following two lines of code at the beginning of the function fixed the issue:
//if element is already editable - ignore
if(elem.attr("contentEditable") === "true")
return;

Wait for page to load when navigating back in jquery mobile

I have a single-page web app built with jQuery Mobile. After the user completes a certain action, I want to programmatically bring them back to a menu page, which involves going back in history and then performing some actions on elements of the menu page.
Simply doing
window.history.go(-1); //or $.mobile.back();
doSomethingWith(menuPageElement);
doesn't work, because the going-back action is asynchronous, i.e. I need a way of waiting for the page to load before calling doSomethingWith().
I ended up using window.setTimeout(), but I'm wondering if there's not an easier way (different pattern?) to do this in jQM. One other option is to listen for pageload events, but I find it worse from code organization point of view.
(EDIT: turns out native js promises are not supported on Mobile Safari; will need to substitute by a 3rd-party library)
//promisify window.history.go()
function go(steps, targetElement) {
return new Promise(function(resolve, reject) {
window.history.go(steps);
waitUntilElementVisible(targetElement);
//wait until element is visible on page (i.e. page has loaded)
//resolve on success, reject on timeout
function waitUntilElementVisible(element, timeSpentWaiting) {
var nextCheckIn = 200;
var waitingTimeout = 1000;
timeSpentWaiting = typeof timeSpentWaiting !== 'undefined' ? timeSpentWaiting : 0;
if ($(element).is(":visible")) {
resolve();
} else if (timeSpentWaiting >= waitingTimeout) {
reject();
} else { //wait for nextCheckIn ms
timeSpentWaiting += nextCheckIn;
window.setTimeout(function() {
waitUntilElementVisible(element, timeSpentWaiting);
}, nextCheckIn);
}
}
});
}
which can be used like this:
go(-2, menuPageElement).then(function() {
doSomethingWith(menuPageElement);
}, function() {
handleError();
});
Posting it here instead of in Code Review since the question is about alternative ways to do this in jQM/js rather than performance/security of the code itself.
Update
To differntiate whether the user was directed from pageX, you can pass a custom parameter in pagecontainer change function. On pagecontainerchange, retrieve that parameter and according bind pagecontainershow one time only to doSomething().
$(document).on("pagecreate", "#fooPage", function () {
$("#bar").on("click", function () {
/* determine whether the user was directed */
$(document).one("pagecontainerbeforechange", function (e, data) {
if ($.type(data.toPage) == "string" && $.type(data.options) == "object" && data.options.stuff == "redirect") {
/* true? bind pagecontainershow one time */
$(document).one("pagecontainershow", function (e, ui) {
/* do something */
$(".ui-content", ui.toPage).append("<p>redirected</p>");
});
}
});
/* redirect user programmatically */
$.mobile.pageContainer.pagecontainer("change", "#menuPage", {
stuff: "redirect"
});
});
});
Demo
You need to rely on pageContainer events, you can choose any of these events, pagecontainershow, pagecontainerbeforeshow, pagecontainerhide and pagecontainerbeforehide.
The first two events are emitted when previous page is completely hidden and before showing menu page.
The second two events are emitted during hiding previous page but before the first two events.
All events carry ui object, with two different properties ui.prevPage and ui.toPage. Use these properties to determine when to run code.
Note the below code only works with jQM 1.4.3
$(document).on("pagecontainershow", function (e, ui) {
var previous = $(ui.prevPage),
next = $(ui.toPage);
if (previous[0].id == "pageX" && next[0].id == "menuPage") {
/* do something */
}
});

Categories

Resources