How can I make jQuery replaceWith() "non-blocking" (async)? - javascript

I'm using the function replaceStr() to replace some strings in the body tag. If the html page is small, the replacing is not noticeable. But if the html page is bigger and more complex you notice it. The replacing is blocking the browser. My question, how is it possible to make the replacing non-blocking? The replacing is not critical to the page, so it can happen in the background, when the browser is not busy. I tried to use async and await, but I think the replaceWith() function can't handle Promises and that's why it's not working with async/await. But how can you do it then?
function replaceStr(myStrArr) {
const container = $('body :not(script)');
myStrArr.map((mystr) => {
container
.contents()
.filter((_, i) => {
return i.nodeType === 3 && i.nodeValue.match(mystr.reg);
})
.replaceWith(function () {
return this.nodeValue.replace(mystr.reg, mystr.newStr);
});
});
}
Thank you for your help.

Your current implementation has a few spots where it could be optimized before going the async route. For example you could get rid of the jQuery dependency. It doesn't help much in your case but adds overhead.
Then currently you're mapping over your replacements and for each over all candidate nodes, replacing the nodeValue each time. This possibly triggers a repaint every time.
Instead you could use a TreeWalker to quickly iterate over the relevant nodes, and only update the nodeValues once.
In my tests, the following runs roughly 16 times faster, than your current code. Maybe that's already enough?
function replaceStr_upd(replacements) {
// create a tree walker for all text nodes
const it = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
// but skip SCRIPTs
acceptNode: node => node.parentNode.nodeName === 'SCRIPT'
? NodeFilter.FILTER_REJECT
: NodeFilter.FILTER_ACCEPT
});
// a helper function
const applyReplacements = initial => replacements.reduce((text, r) => text.replace(r.reg, r.newStr), initial);
// iterate over all text node candidates
while (it.nextNode()) {
// but only update once per node:
it.currentNode.nodeValue = applyReplacements(it.currentNode.nodeValue);
}
}

Related

Adding dynamically third party javascript widgets to the web app

I am working on a React js project where I should dynamically add third party scripts (widgets) to the web app.
Widgets include any kind of third party platforms: Twitter, Instagram, Youplay, Youtube etc.
The current code that I have looks like this:
appendThirdPartyScript(externalScript) {
// Create an element outside the document to parse the string with
const head = document.createElement("head");
// Parse the string
head.innerHTML = externalScript;
// Copy those nodes to the real `head`, duplicating script elements so
// they get processed
let node = head.firstChild;
while (node) {
const next = node.nextSibling;
if (node.tagName === "SCRIPT") {
// Just appending this element wouldn't run it, we have to make a fresh copy
const newNode = document.createElement("script");
if (node.src) {
newNode.src = node.src;
}
while (node.firstChild) {
// Note we have to clone these nodes
newNode.appendChild(node.firstChild.cloneNode(true));
node.removeChild(node.firstChild);
}
node = newNode;
}
document.head.appendChild(node);
node = next;
}
}
So basically the scripts urls comes from the backend/api and that is a list of scripts smth like ['http://twitter-widget.js', 'http://instagram-widget.js']
So since I have an array of scripts as string I use a for loop to go thru each of element and call appendThirdPartyScript('somescript')
data.externalScripts.map(script => {
this.appendThirdPartyScript("" + script);
});
This solution worked for almost all cases until I came to this case:
['http://embed.tt.se/v10/tt-widget.js', 'new tt.TTWidget({WhereToAddWidget: 'tt-quiz-XMuxmuWHc',type: 'quiz',ID: 'xxx',clientID: 'xxx',});']
So basically the error I get is:
tt is not a function
I am assuming that the first script hasn't completed loading (in this case http://embed.tt.se/v10/tt-widget.js) and the next one is trying to call something that does not exists.
When I try to hard code http://embed.tt.se/v10/tt-widget.js within head tag in index.html directly than it works!
So this approach of dynamically adding third party widgets is not reliable. Anyone can let me know if my current code needs to be changed or any suggestion would be pretty much appreciated!
This seems to work but probably not best solution:
const delayTime = 500;
externalScriptArray.forEach((script, index) => {
setTimeout(() => {
this.appendNewTagElementToDom(script);
}, delayTime*index);
});

getAllData does not include element just inserted, even after callback

So I am using NeDB as a data store for a simple little project, and I want to create a little API that inserts something new and returns the list of all items in the datastore, including the new one.
async function willAddNewItem() {
// db is NeDB Datastore
const existing = db.getAllData();
const sortedIds = existing
.map(item => item.id)
.sort((one, another) => one - another);
const id = sortedIds.length === 0
? 0
: sortedIds.slice(-1)[0] + 1;
const newItem = { id, foo: 'bar' };
await new Promise(resolve => db.insert(newItem, () => resolve()));
return db.getAllData()
.sort((one, another) =>
new Date(one.updatedAt).getTime() - new Date(another.updatedAt).getTime()
);
}
However, every time I call this function, I get the list of items I had before inserting the new one. On the next call, the item I added last time would be there, but not the new one. Refreshing my page (which results in calling db.getAllData() again to populate the initial page) shows everything it should. It’s only missing immediately after I insert it. So it would appear that the callback on db.insert is not actually waiting until the insertion is complete (nor is there any documentation that I can find about this).
Adding an index to id fixes this, and I do want an index on id so that is good, but I still want to know why this is happening/where I have to be careful about it happening in the future. For that matter, I’m a little worried that the only reason adding the index to id worked is because it happened to be a little faster, so it was “done” before I called db.getAllData() when it wasn’t before—and that this may not be consistent.
I could use something like
const afterAdding = await new Promise(resolve =>
db.insert(newItem, (_, newDoc) => resolve([...existing, newDoc]))
);
and return the sorted afterAdding, but this seems a little dubious to me (I’d really like to get exactly the db’s state rather than try to “recreate” it myself, assuming that insert has done exactly and only what I expected), and in any event I would like to know more about how NeDB works here.
Also, yes, I know I’m ignoring possible errors on the insert (I have checked to see if this is the cause; it is not), and I probably don’t want .getAllData() at all but rather some .find query. This is a basic test case as I get familiar with NeDB.

How to set the order elements are appended to a documentFragment when being returned async?

Odd question I guess...
I'm building my DOM in memory and make heavy use of promises. Say I have this inside a for... loop:
target = document.createDocumentFragment();
promises = [], pass, skip, store;
for (i = 0; i < foo; i += 1) {
element = foo[i];
// set promise
promises[i] = app.setContent(element, {}, update)
.then(function(response) {
// HELP!
if (pass) {
target.appendChild(store)
store = undefined;
skip = undefined;
pass = undefined;
}
if (response.tagName !== undefined) {
pass = true;
}
if (skip === undefined && response.tagName === undefined) {
store = response;
skip = true;
} else {
target.appendChild(response);
}
});
RSVP.all(promises).then(continue...
The loop above runs 3x returning two div tags and a documentFragment. Without promises, no problem, my structure is
<div toolbar>
<fragment = form>
<div toolbar>
Problem is, when I converted to async I have no more say in the order items get appended forcing me to stupid things as above, where I'm finding my fragment, store it in store, set a skip parameter, append my first div, which sets pass, which will allow my stored fragment to be appended when the next promises "comes in"... what a waste of code...
Question:
Is there any way to properly set the order of items in a generic way when all items are being returned async? If not, how do I move items? Since I'm working in memory, I can select using querySelector/qsAll but I don't have a lot of other options? Any ideas?
(And please don't suggest to put in the DOM and then shuffle :-))
Thanks!
I'll explain in words ...
On making each async request, also create a container, in the right place, for the requested data to go when it eventually arrives. This container will typically be a <div> or a <span>.
Keep a reference to the container (typically in a closure) such that the async response handler knows which container corresponds to which data.

how to preserve style when cloning a node

I want to take an html document (a book chapter) and separate it into pages (an array of DIV, each containing a page of html content that will fit within the prescribed dimensions of the DIV). I walk the DOM with the following code (found on this site!).
function walk(node, func)
{
func(node);
node = node.firstChild;
while (node)
{
walk(node, func);
node = node.nextSibling;
}
};
The func function is called test and is below.
function test(node)
{
var copy=node.cloneNode(false);
currentPageInArray.appendChild(copy);
//Test if we still fit
if( $(currentPageInArray).height() <= maxPageHeight )
{
//All good
}
else
{
//We dont fit anymore
//Remove node that made us exceed the height
currentPageInArray.removeChild(copy);
createNewPage();
currentPageInArray.appendChild(copy); //into new page
}
}
My pages get generated correctly, however, I lose all styles such as italic, bold, headers, etc. If I try clone(true), many elements get duplicated multiple times. How can I fix this? Thanks in advance.
You may retrieve the current layout of every element using currentStyle(IE<9) or getComputedStyle(Others) and apply it to the cloned elements.

Dojo extending dojo.dnd.Source, move not happening. Ideas?

NOTICE: THIS IS SOLVED, I WILL PUBLISH THE SOLUTION HERE ASAP.
Hey all,
Ok... I have a simple dojo page with the bare essentials. Three UL's with some LI's in them. The idea si to allow drag-n-drop among them but if any UL goes empty due to the last item being dragged out, I will put up a message to the user to gie them some instructions.
In order to do that, I wanted to extend the dojo.dnd.Source dijit and add some intelligence. It seemed easy enough. To keep things simple (I am loading Dojo from a CDN) I am simply declating my extension as opposed to doing full on module load. The declaration function is here...
function declare_mockupSmartDndUl(){
dojo.require("dojo.dnd.Source");
dojo.provide("mockup.SmartDndUl");
dojo.declare("mockup.SmartDndUl", dojo.dnd.Source, {
markupFactory: function(params, node){
//params._skipStartup = true;
return new mockup.SmartDndUl(node, params);
},
onDndDrop: function(source, nodes, copy){
console.debug('onDndDrop!');
if(this == source){
// reordering items
console.debug('moving items from us');
// DO SOMETHING HERE
}else{
// moving items to us
console.debug('moving items to us');
// DO SOMETHING HERE
}
console.debug('this = ' + this );
console.debug('source = ' + source );
console.debug('nodes = ' + nodes);
console.debug('copy = ' + copy);
return dojo.dnd.Source.prototype.onDndDrop.call(this, source, nodes, copy);
}
});
}
I have a init function to use this to decorate the lists...
dojo.addOnLoad(function(){
declare_mockupSmartDndUl();
if(dojo.byId('list1')){
//new mockup.SmartDndUl(dojo.byId('list1'));
new dojo.dnd.Source(dojo.byId('list1'));
}
if(dojo.byId('list2')){
new mockup.SmartDndUl(dojo.byId('list2'));
//new dojo.dnd.Source(dojo.byId('list2'));
}
if(dojo.byId('list3')){
new mockup.SmartDndUl(dojo.byId('list3'));
//new dojo.dnd.Source(dojo.byId('list3'));
}
});
It is fine as far as it goes, you will notice I left "list1" as a standard dojo dnd source for testing.
The problem is this - list1 will happily accept items from lists 2 & 3 who will move or copy as apprriate. However lists 2 & 3 refuce to accept items from list1. It is as if the DND operation is being cancelled, but the debugger does show the dojo.dnd.Source.prototype.onDndDrop.call happening, and the paramaters do look ok to me.
Now, the documentation here is really weak, so the example I took some of this from may be way out of date (I am using 1.4).
Can anyone fill me in on what might be the issue with my extension dijit?
Thanks!
If you use Dojo XD loader (used with CDNs), all dojo.require() are asynchronous. Yet declare_mockupSmartDndUl() assumes that as soon as it requires dojo.dnd.Source it is available. Generally it is not guaranteed.
Another nitpicking: dojo.dnd.Source is not a widget/dijit, while it is scriptable and can be used with the Dojo Markup, it doesn't implement any Dijit's interfaces.
Now the problem — the method you are overriding has following definition in 1.4:
onDndDrop: function(source, nodes, copy, target){
// summary:
// topic event processor for /dnd/drop, called to finish the DnD operation
// source: Object
// the source which provides items
// nodes: Array
// the list of transferred items
// copy: Boolean
// copy items, if true, move items otherwise
// target: Object
// the target which accepts items
if(this == target){
// this one is for us => move nodes!
this.onDrop(source, nodes, copy);
}
this.onDndCancel();
},
Notice that it has 4 arguments, not 3. As you can see if you do not pass the 4th argument, onDrop is never going to be called by the parent method.
Fix these two problems and most probably you'll get what you want.
In the end, I hit the Dojo IRC (great folks!) and we ended up (so far) with this...
function declare_mockupSmartDndUl(){
dojo.require("dojo.dnd.Source");
dojo.provide("mockup.SmartDndUl");
dojo.declare("mockup.SmartDndUl", dojo.dnd.Source, {
markupFactory: function(params, node){
//params._skipStartup = true;
return new mockup.SmartDndUl(node, params);
},
onDropExternal: function(source, nodes, copy){
console.debug('onDropExternal called...');
// dojo.destroy(this.getAllNodes().query(".dndInstructions"));
this.inherited(arguments);
var x = source.getAllNodes().length;
if( x == 0 ){
newnode = document.createElement('li');
newnode.innerHTML = "You can drag stuff here!";
dojo.addClass(newnode,"dndInstructions");
source.node.appendChild(newnode);
}
return true;
// return dojo.dnd.Source.prototype.onDropExternal.call(this, source, nodes, copy);
}
});
}
And you can see where I am heading, I put in a message when the source is empty (client specs, ug!) and I need to find a way to kill it when something gets dragged in (since it is not, by definition, empty any more ona incomming drag!). That part isnt workign so well.
Anyway, the magic was not to use the onDnd_____ functions, but the higher level one and then call this.inherited(arguments) to fire off the built in functionality.
Thanks!
dojo.require("dojo.dnd.Source");
dojo.provide("mockup.SmartDndUl");
dojo.declare("mockup.SmartDndUl", dojo.dnd.Source, {
Dojo require statement and declare statement are next to next. I think that will cause dependencies problem.
the dojo require statement should go outside onload block and the declare statement should be in onload block.

Categories