Reputation: 11725
I have the following code:
function load(lab, el) {
return Promise.all([Util.loadHtml(lab), Util.loadScript(lab)])
.then(function(responses) {
parse(responses[0], el, responses[1]);
});
}
function parse(html, parent, context) {
var children = [].slice.call(html.childNodes).filter(function (item) { return item.nodeType === 1 || item.nodeType === 3; });
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.tagName.indexOf('-') >= 0) {
load(child.tagName.toLowerCase(), parent);
}
else {
var parsedNode = parseNode(child, context);
parent.appendChild(parsedNode);
if (child.hasChildNodes())
parse(child, parsedNode, context);
}
}
}
Basically, that's what it should do:
load
function, that will import two files, one html
and one js
, and when the promise of those requests is fullfilled, it calls a function named parse
which will loop into the HTML and will parse some strings with the class declared in the JS file.<my-element>
, and then, it will try to load my-element.html
and my-element.js
, and it will loop inside that HTML too.The problem
Since the load
function returns a promise, and I'm calling it synchronously, it immediately returns, and for that reason, the children is not placed inside the correct parents.
If I was doing that in C#, for example, or with the ES7 async
and await
keywords, that would be pretty easy. But I have no idea how can I call that load
function asynchronously. Any guesses?
Upvotes: 2
Views: 1751
Reputation: 11725
In the end, it was simpler than I thought it would be:
function load(lab, el) {
return Promise.all([Util.loadHtml(lab), Util.loadScript(lab)])
.then(function(responses) {
return parse(responses[0], el, responses[1]); // return here
});
}
function parse(html, parent, context) {
var children = [].slice.call(html.childNodes).filter(function (item) { return item.nodeType === 1 || item.nodeType === 3; });
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.tagName.indexOf('-') >= 0) {
return load(child.tagName.toLowerCase(), parent); // return here
}
else {
var parsedNode = parseNode(child, context);
parent.appendChild(parsedNode);
if (child.hasChildNodes())
parse(child, parsedNode, context);
}
}
}
Since my parsing must be synchronous (because of the order etc), and the only thing I needed was waiting for the load
function to finish before going back to parse
, the only things I changed was to instead of directly calling the parse
function inside the load
one and vice-versa, I'm now using the return
, since then it will wait for the execution, before going back to the caller.
Other thing that worked and it was even better for my use case: I ended up creating a clone of my custom element, and appending it to the parent, calling the loading function passing it. Doing that, I could load all its children async and without the problem of not being attached to the DOM.
Faster execution, and better readability!
Upvotes: 0
Reputation: 9813
You can use .reduce to achieve that:
function load(lab, el) {
return Promise.all([Util.loadHtml(lab), Util.loadScript(lab)])
.then(function(responses) {
return parse(responses[0], el, responses[1]);
});
}
function parse(html, parent, context) {
var children = [].slice.call(html.childNodes).filter(function (item) { return item.nodeType === 1 || item.nodeType === 3; });
// What this return is a promise chained through all of its children
// So until all children get resolved, this won't get resolved.
return children.reduce(function(promise, child, idx) {
var childPromise = null;
if (child.tagName.indexOf('-') >= 0) {
// Start to load the contents, until childPromise is resolved, the final
// can't be resolved.
childPromise = load(child.tagName.toLowerCase(), parent);
} else {
var parsedNode = parseNode(child, context);
parent.appendChild(parsedNode);
// If it has child, also make it return a promise which will be resolved
// when child's all children parsed.
if (child.hasChildNodes()) {
childPromise = parse(child, parsedNode, context);
}
}
// It's either null, which means it'll be resolved immediately,
// or a promise, which will wait until its childs are processed.
return promise.then(function() {
return childPromise;
});
}, Promise.resolve());
}
Then, when iteratin through the children, it'll keep chaining the promise, each child can either load or parse independently, and until all children is resolved, the promise return from parse
get resolved. So you can now use it like:
parse(THE PARAM OF ROOT).then(function() {
// All the big parents's children are now parsed.
console.log('All done');
});
EDITED: as Bergi suggests, Promise.all is better then .reduce, as it'll reject immediately when any of the children(grandchildren) fails. And as The more acceptable answer is posted, I'll just give a link to it, instead of add the .all
version.
And JavaScript Promises#chaining may help you too.
Upvotes: 1
Reputation: 664195
If a function is asynchronous, it should return a promise. Always. Even (or: especially) in then
callbacks.
If you produce multiple promises in that loop, you can await them via Promise.all
:
function load(lab, el) {
return Promise.all([Util.loadHtml(lab), Util.loadScript(lab)])
.then(function(responses) {
return parse(responses[0], el, responses[1]);
// ^^^^^^
});
}
function parse(html, parent, context) {
var children = [].slice.call(html.childNodes).filter(function (item) { return item.nodeType === 1 || item.nodeType === 3; });
return Promise.all(children.map(function(child, i) {
//^^^^^^^^^^^^^^^^^^
if (child.tagName.indexOf('-') >= 0) {
return load(child.tagName.toLowerCase(), parent);
// ^^^^^^
} else {
var parsedNode = parseNode(child, context);
parent.appendChild(parsedNode);
if (child.hasChildNodes())
return parse(child, parsedNode, context);
// ^^^^^^
}
}));
}
If I was doing that in C#, for example, or with the ES7 async and await keywords, that would be pretty easy. But I have no idea how can I call that
load
function asynchronously
Yeah, you really should consider using those. Or you can emulate them with ES6 generator functions and a runner (as provided by many popular promise libraries). But you're using a transpiler anyway, right?
Writing load
would be quite easy with them:
async function load(lab, el) {
var responses = await Promise.all([Util.loadHtml(lab), Util.loadScript(lab)]);
return parse(responses[0], el, responses[1]);
}
Upvotes: 3
Reputation: 14423
I think this is where you got it wrong:
if (child.tagName.indexOf('-') >= 0) {
load(child.tagName.toLowerCase(), parent);
}
You are passing the parent
of the child
object to be the parent
of the grandchild
object. You probably need to pass child
as parent of the grandchild
.
Upvotes: 0