问题
Consider the following test-element
web component definition (vanilla JavaScript running on Google Chrome, not Polymer),
that creates a simple component with width=500px
. Its attachedCallback
function outputs its width to the console, and then sets up an asynchronous delay to do it again:
test-element.html
<style> test-element { display: inline-block; width: 500px; } </style>
<script>
(function (window, document) {
var proto = Object.create(HTMLElement.prototype);
proto.attachedCallback = function () {
// Direct output.
console.log("(1) test-element.width = ", this.offsetWidth);
// Delayed output.
window.setTimeout(function () { console.log("(2) test-element.width = ", this.offsetWidth); }.bind(this), 0);
};
document.registerElement('test-element', {prototype: proto});
})(window, document);
</script>
I then create a page that imports test-element
, and outputs its width yet again from an inline script tag:
index.html
<!DOCTYPE html>
<html>
<head>
<link rel="import" href="test-element.html">
</head>
<body>
<test-element></test-element>
<script>
var testElement = document.querySelector('test-element');
console.log("(3) test-element.width = ", testElement.offsetWidth);
</script>
</body>
</html>
The output to the console is:
(1) test-element.width = 0
(3) test-element.width = 500
(2) test-element.width = 500
This goes to prove that attachedCallback
(1) is called before layouts and paints, which sometimes is really what I want.
For example, if I want to change its width to 100 pixels, I can make testElement.style.width = "100px"
, and I don't need not worry about
the component flashing its previous 500px width before the callback has a chance to make the change.
However, attachedCallback
being called before layouts sometimes is NOT what I want.
For example, if test-element
contains a grid, and it needs to draw cells, then I need to know its width so that I can calculate how many cells
fit the available space. But I don't have this information during the attachedCallback
, and I can't seem to grasp how to get this information.
As you can see, a 0ms delay (2) seems to trigger the layout calculation in this very simple example. But as soon as I start doing more complicated things (for example when I have nested web components and asynchronous HTML imports), the 0ms delay is not enough, and I have to change it to a 100ms delay, and then more.
My question is: How can I reliably get the width of the component (or any other computed styles), as soon as possible?
回答1:
I seem to have found the solution, using requestAnimationFrame
:
proto.attachedCallback = function () {
// This never works:
console.log("(1) test-element.width = ", this.offsetWidth);
// This works sometimes:
window.setTimeout(function () { console.log("(2) test-element.width = ", this.offsetWidth); }.bind(this), 0);
// SOLUTION - This seems to always work.
window.requestAnimationFrame(function () { console.log("(4) test-element.width = ", this.offsetWidth); }.bind(this));
};
Note: This is so far working OK for me, no matter how many web components I nest, and no matter how complicated I chain my HTML imports. However... does this always work? HTML parsing of HTML imports is parallelized (according to user https://stackoverflow.com/users/274673/ebidel here: http://www.html5rocks.com/en/tutorials/webcomponents/imports) and I don't perfectly understand the situation to be absolutely sure of this solution.
回答2:
In your example above, the fastest way to get the (custom) element width
in a <script>
tag just after the element, exactly what you did in your test (3):
<test-element></test-element>
<script>
var testElement = document.querySelector('test-element');
console.log("(3) test-element.width = ", testElement.offsetWidth);
</script>
It's because HTML elements are rendered sequencially by the browser. So the script content will be executed just after the calculation of the <test-element>
width
and other layout properties.
(1) test-element.width = 0
(3) test-element.width = 500
HTMLImportsLoaded //with polyfill
DomContentLoaded
window.onload
(2) test-element.width = 500
WebComponentsReady //with polyfill
NB: Other browser than Chrome will behave differently.
Update
Be careful not getting the computed style values too soon in the rendering, especially if another element is using the requestAnimationFrame
method, too.
Below an example that shows a web component cannot always rely on this hack to know itself: the grid element doesn't know its rendered width
at step (4) yet.
(function(window, document) {
var proto = Object.create(HTMLElement.prototype);
proto.attachedCallback = function() {
// Direct output.
console.log("(1) %s.width = ", this.id, this.offsetWidth);
this.innerHTML = "Element: " + this.id
// Delayed output.
window.setTimeout(function() {
console.log("(2) %s.width = %s ", this.id, this.offsetWidth);
}.bind(this), 0);
window.requestAnimationFrame(function() {
if (this.id == "side")
this.classList.add("large")
console.log("(4) %s.width = %s", this.id, this.offsetWidth);
}.bind(this));
};
document.registerElement('test-element', {
prototype: proto
});
})(window, document);
test-element {
display: inline-block;
border: 1px solid red;
width: 500px;
}
.large {
width: 300px;
}
<!DOCTYPE html>
<html>
<head>
</head>
<body style="display:flex">
<test-element id="grid"></test-element>
<test-element id="side"></test-element>
<script>
var testElement = document.querySelector('test-element');
console.log("(3) %s.width = %s", testElement.id, testElement.offsetWidth);
window.onload = function() {
console.log("window.onload")
}
</script>
</body>
</html>
来源:https://stackoverflow.com/questions/33602247/how-can-a-web-component-get-its-own-styles-width-etc-as-soon-as-possible