Manipulating closure scoped dom element inside promise callback - javascript

I am writing a Chrome extension to modify site contents.
I am fetching some data from a backend API through the background script and then updating the website contents to reflect that data.
Initially, I was confused that after modifying the elements' innerHTML the change wasn't reflected in the page.
After some debugging I found out element.isConnected is false inside the callback function.
Now, I'm unable to figure out a way to fix this and update the DOM elements.
The basic outline is that I'm reading rows of data from a table and for each row I'm reading the cell contents and updating cell contents after the API returns. Each row of the table is stored as an element of an array and is operated on by a map function.
Here is my relevant code:
let tableRows = document.querySelectorAll(".tablerow");
let tableRowsArray = Array.from(tableRows);
tableRowsArray.map(function (row) {
let tableCells = row.innerHTML.split("</td>");
(async () => {
console.log(row.isconnected); //returns true here
const response = await chrome.runtime.sendMessage(/*{some data}*/);
let fetcheda = response.a;
tableCells.splice(12, 0, "<td><output>" + fetcheda + "</output></td>");
console.log(row.isConnected); //returns false here
row.innerHTML = tableCells.join("");
console.log(row); //the correct row is modified correctly but does not reflect in the page
})();
});
I tried .then() for the promise instead of await and the problem remains. I also replace chrome.runtime.sendMessage() with a timeout promise and the problem remains the same.
Here is the code using .then():
let tableRows = document.querySelectorAll(".tablerow");
let tableRowsArray = Array.from(tableRows);
tableRowsArray.map(function (row) {
let tableCells = row.innerHTML.split("</td>");
console.log(row.isconnected); //returns true here
chrome.runtime.sendMessage(/*{some data}*/).then((response) => {
let fetcheda = response.a;
tableCells.splice(12, 0, "<td><output>" + fetcheda + "</output></td>");
console.log(row.isConnected); //returns false here
row.innerHTML = tableCells.join("");
console.log(row); //the correct row is modified correctly but does not reflect in the page
})
});
I believe that somehow some properties of the row variable are stripped inside the .then() callback as row now becomes closure scoped instead of local scoped. I would like to know some way to be able to modify the DOM inside the .then() callback. And I also cannot simply get the respective row again for which the API request was made.

Related

Using the DOM to Call Multiple Functions and Append Elements

I'm currently practicing with using the DOM and manipulating it. Right now, I'm practicing with creating code for a general clicker style game. The tools goes up every time that tool is clicked and I have a builder that a player can purchase and open/unlock (if the count of the tools is enough to purchase).
I am having a hard time trying to call multiple functions, but also while manipulating the DOM by appending div elements (if that makes sense). I know that I will have to loop thru the data below and then outside of the loop I would need to append some div elements to the builder container.
The data that I'm working with:
data = {
tools: 200,
builders: [
{ id: 'builder_A', price: 100, unlocked: false },
{ id: 'builder_B', price: 400, unlocked: false },
{ id: 'builder_C', price: 700, unlocked: false }
]
};
The specs that I'm working with:
calls document.getElementById()
appends some builder div elements to the builder container
const builderContainer = document.getElementById('builder_container');
assert.isAbove(builderContainer.childNodes.length, 0);
unlocks any locked builders that need to be unlocked
code.renderBuilders(data);
expect(data.builders[0].unlocked).to.be.equal(true);
expect(data.builders[1].unlocked).to.be.equal(true);
expect(data.builders[2].unlocked).to.be.equal(false);
only appends unlocked builders
code.renderBuilders(data);
const builderContainer = document.getElementById('builder_container');
expect(builderContainer.childNodes.length).to.be.equal(2);
expect(builderContainer.childNodes[0].childNodes).to.have.length(5);
deletes the builder container's children before appending new builders
const builderContainer = document.getElementById('builder_container');
const fakeBuilder = document.createElement('div');
container.appendChild(fakeBuilder);
code.renderBuilders(data);
expect(builderContainer.childNodes.length).to.be.equal(2);
expect(builderContainer.childNodes[0].childNodes).to.have.length(5);
Here is the code that I've written thus far:
function renderBuilders(data) {
let builderContainer = document.getElementById('builder_container');
for (let i = 0; i < data.length; i++) {
const newBuilder = currentBuilder; // storing the current builder in a new variable, but not sure if this would be the correct route?
currentBuilder = makeBuilderDiv(); // which already creates a builder container div
let deletedBuilder = deleteAllChildNodes(); // this function deletes the builder container's children before appending new builders
let unlockedBuilder = getUnlockedBuilders(); // this function would unlock any locked builders that need to be unlocked
}
builderContainer.appendChild(builder);
}
However, but when I'm trying to append outside of the loop, I'm getting this error (for a different function that was already created):
function updateToolsPerSecondView(tps) {
const tpsView = document.getElementById('tps');
tpsView.innerText = tps;
}
TypeError: Attempted to wrap getElementById which is already wrapped
I believe I'm going to have to call those 3 functions in the loop and possibly create new variables to store, but I can't seem to figure out what I'm missing in the loop itself and why I'm getting the error.
Any help would be appreciated!
Whether your code execute in the loop or not, getElementById should be executed once. The error message says TypeError: Attempted to wrap getElementById which is already wrapped, which one of tags with the function has a tag as itself. In the function updateToolsPerSecondView it changed tpsView inner text to a html tag. It should be a string. You may change like from
tpsView.innerText = tps;
to
tpsView.innerText = tps.innerText;
tpsView.innerText can not have tag as its value.

Using control.setDisabled() after promise

I'm writing some JS for Dynamics 365 which disables (locks) the fields on the selected editable subgrid row.
The method to do this is .setDisabled() (Documentation). I can run the following method which will lock all the fields upon selecting a row:
function onGridRowSelected(context){
context.data.entity.attributes.forEach(function (attr) {
attr.controls.forEach(function (myField) {
myField.setDisabled(foundResponse);
})
});
}
The issue I am having is trying to run the above following a promise. I have the following code which will pass the result of a promise into my disable fields methods:
var gridContext;
function onGridRowSelected(context){
gridContext = context.getFormContext();
//Retrieve the record we want to check the value on
Xrm.WebApi.retrieveMultipleRecords("ms_approvalquery", "?$select=ms_responsetext&$top=1&$orderby=createdon desc")
.then(result => disableOrEnableFields(result));
}
function disableOrEnableFields(result){
//Check if the record found has a ms_responsetext != null
var foundResponse = false
if (result.entities[0].ms_responsetext != null){
foundResponse = true;
}
//Either disable/enable all the row columns depending on the value retrieved from the above
gridContext.data.entity.attributes.forEach(function (attr) {
attr.controls.forEach(function (myField) {
myField.setDisabled(foundResponse);
})
});
}
When stepping through debug, I can see that myField.setDisabled(true); is getting called but nothing is happening. Is this because it's on a separate thread? How do I get back to the main thread with the result of my promise?
Note: Using Async/Await doesn't work either - it gives the same results.
we had similar issues few days back, unfortunately Async/Await/promise call does not respect grid control, you will have to go by old/classic Sync call way and then it shall work. Let me know if this solves your problem.

How to handle events in dynamically created elements in js

I want to add the click event to a created Element dynamically that when user click on button create some elements (elements that show in code below) and when user click on element named remov must run function named deltion BUT that doesnt work.How can I implemented that ?
I use Vue js
methods: {
showinfo: function() {
db.collection("Studnets")
.get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
const student = document.createElement("tr");
const email = document.createElement("td");
email.innerText = doc.data().email;
const name = document.createElement("td");
name.innerText = doc.data().name;
const phone = document.createElement("td");
phone.innerText = doc.data().phone;
const stage = document.createElement("td");
stage.innerText = doc.data().stage;
const remov = document.createElement("button");
const pic = document.createElement("img");
pic.setAttribute("src","/dist/delete.svg?afdf9975229cdc15458e851b478bb6cb");
remov.classList.add("del");
//the problem
remov.addEventListener("click", this.deltion());
student.appendChild(email);
student.appendChild(name);
student.appendChild(phone);
student.appendChild(stage);
student.appendChild(remov);
remov.appendChild(pic);
document.getElementById("studentList").appendChild(student);
},
deltion: function(e) {
const rawStudent = e.target;
const raw = rawStudent.parentElement;
console.log(raw);
raw.remove();
}
There are three issues with your code.
First Issue: invoking a function on the event listener
you are calling the deltion (maybe you mean deletion :P) function when you register the event listener.
remov.addEventListener("click", this.deltion());
the correct form is
remov.addEventListener("click", this.deltion);
Because you want to pass the function body to the event listener, not the function result.
(you can wrap the function in an arrow function if you want to call it, but at the end is the same).
Second Issue: this is not this
If you fix the first one, you'll find another one (the life of programmers). this is a special keyword in js, the context of this will change depending on the caller.
showinfo is called the this keyword refers to the component instance.
db.collection("Studnets").get().then(function(querySnapshot) {}) promise is called and on resolve it will call the function that has querySnapshot parameter. **this**
keyword context changes.
you iterate the collection with a foreach querySnapshot.forEach(function(doc) {}) **this** keyword context is changed again.
A solution will be to use arrow functions so this don't get bonded to the parent function.
db.collection("Studnets").get().then(querySnapshot => {
querySnapshot.data.forEach(doc => {
// code
})
})
If for some reason you can't use arrow functions, you can add a variable that "shadows" the context of the this keyword on the showinfo function.
showinfo() {
const vm = this;
api.getStudentsCollection().then(function(querySnapshot) {
querySnapshot.data.forEach(function(doc) {
// setup code
remov.addEventListener("click", vm.deltion);
// setup code
});
});
}
Third Issue: clicking the arrow image will delete the button but not the tr
Use currentTarget instead of the target, the target is the element the user click, it can be any element inside the button like the image, currentTarget is the element that the event listener is attached aka the button.
{
deltion: function(e) {
const rawStudent = e.currentTarget;
const raw = rawStudent.parentElement;
console.log(raw);
raw.remove();
}
}
Unsolicited Advice
Vue excels with its simplicity and declarative code. You can solve the problem like your original code, but there is a simpler way to let the component manage its state.
I replicated your code with the original version fixed and the simplest vue way (with a simple empty and loading state toggle). Hope you find it useful and learn from this. :) https://codesandbox.io/s/student-list-ammar-yasir-b5sxo?file=/src/App.vue
Learning references
https://javascript.info/
https://dev.to/cilvako/this-keyword-in-javascript-an-explanation-1con

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>

Javascript: Values of array disappear after referring to it from other function

I currently try to build a form with javascript which has two functionalities:
1) Adding elements dynamically to a list
2) Identify through a button click a certain element (e.g. with the highest value)
See (wanted to add pictures directly to my post, but I am lacking StackOverflow reputation - so here are they as links):
https://i.ibb.co/KxvV5Ph/Bildschirmfoto-2019-11-03-um-19-12-51.png
First functionality works fine (see above, added installations). The second doesnt. My plan was the following:
1) When an element gets added to the list I also push it as an object of the class "installation" to the array installations = []
2) When I click on "Identify Longest Duration" I iterate through a map function over the installations array (and output the highest value as an alert).
Unfortunately, the installations array is empty when I call it from another function.
Get values from form
var instStorage = document.getElementById("instStorage");
var instMasse = document.getElementById("instMasse");
var instPrice = document.getElementById("instPrice");
var instDischarge = document.getElementById("instDischarge");
const installations = [] ; // empty installations array
Adding values to DOM, Calling another function to add values to installations array
const createInstallation = () => {
... (working code to add vars from 1) to list element in DOM)...
addInstallation(); // calling another function to add installation to installations array
}
Class Definition of installation
class installation {
constructor(storage, masse, price, discharge) {
this.storage = storage;
this.masse = masse;
this.price = price;
this.discharge = discharge;
}
... (getter functions here) ...
summary = () => {
return `Installation Specs: Storage ${this.getStorage()},
Mass ${this.getMasse()}, Price ${this.getPrice()} and Discharge
Rate
${this.getDischarge()}`;
}
}
Adding installation to installations array
const addInstallation = () => {
installations.push(new installation(instStorage, instMasse, instPrice, instDischarge));
(...)
}
When I call for test purposes my summary function within the createInstallation() function (after calling addInstallation()) everything works fine; the first element of the installations array gets displayed:
alert(installations[0].summary());
See:
https://i.ibb.co/Khc6R7r/Bildschirmfoto-2019-11-03-um-19-32-41.png
When I call the summary function from the event listener of the "Identify Longest" button the installations array is suddenly empty, even though I added an element a second before (which should have been added to the installations array).
See:
https://i.ibb.co/80bTFWY/Bildschirmfoto-2019-11-03-um-19-36-48.png
I am afraid that's a problem of scope; but I don't see how to fix it.
Help is appreciated :-)
Thanks a lot!
Get values from form
var instStorage = document.getElementById("instStorage");
That is what is not happening. getElementById() gets you an element, by id. Not its value.
Assuming that instStorage and friends are input elements (which is not shown unfortunately), you may want to change the code to actually get their value:
var instStorage = document.getElementById("instStorage").value;
var instMasse = document.getElementById("instMasse").value;
var instPrice = document.getElementById("instPrice").value;
var instDischarge = document.getElementById("instDischarge").value;

Categories

Resources