https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-autonomous-example:htmlelement
In the specification they've provided an example for Creating an autonomous custom element. However, they've left _updateRendering() method implementation for the readers.
class FlagIcon extends HTMLElement {
constructor() {
super();
this._countryCode = null;
}
static observedAttributes = ["country"];
attributeChangedCallback(name, oldValue, newValue) {
// name will always be "country" due to observedAttributes
this._countryCode = newValue;
this._updateRendering();
}
connectedCallback() {
this._updateRendering();
}
get country() {
return this._countryCode;
}
set country(v) {
this.setAttribute("country", v);
}
_updateRendering() {
// Left as an exercise for the reader. But, you'll probably want to
// check this.ownerDocument.defaultView to see if we've been
// inserted into a document with a browsing context, and avoid
// doing any work if not.
}
}
An issue has been raised to provide the remaining implementation for better understanding of the topic and quickly move on.
Issue: https://github.com/whatwg/html/issues/3029
What code can we put there to get the required functionality?
CodePudding user response:
Here is the complete code to achieve the same requirements:
<!DOCTYPE html>
<html lang="en">
<body>
<flag-icon country="in"></flag-icon><br>
<flag-icon country="nl"></flag-icon><br>
<flag-icon country="us"></flag-icon><br>
</body>
<script>
class FlagIcon extends HTMLElement {
constructor() {
super();
this._countryCode = null;
}
static observedAttributes = ["country"];
attributeChangedCallback(name, oldValue, newValue) {
// name will always be "country" due to observedAttributes
this._countryCode = newValue;
this._updateRendering();
}
connectedCallback() {
this._updateRendering();
}
get country() {
return this._countryCode;
}
set country(v) {
this.setAttribute("country", v);
}
_updateRendering() {
//**remaining code**
if (this.ownerDocument.defaultView && !this.hasChildNodes()) {
var flag = document.createElement("img");
flag.src = "https://flagcdn.com/24x18/" this._countryCode ".png";
this.appendChild(flag);
}
}
}
customElements.define("flag-icon", FlagIcon);
</script>
</html>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
Note: images may take time to load depending on the internet speed.
Let me know if I've missed anything.
CodePudding user response:
Your solution fails, once the flag is set you can never change the value.
That "exercise" is old.. very old.. and contrived to show everything Custom Elements can do.
And it is plain wrong.. key is WHEN attributeChanged
runs, and what the old/new values are
And attributeChangedCallback
runs BEFORE connectedCallback
; that is why https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected was added.
Your code gets 3 parameters in attributeChangedCallback
, but you can't do anything with them, because execution always goes to the _updateRendering
method.
If the point of the exercise is to learn when Observed attributes
change I would use:
Code also available in JSFiddle: https://jsfiddle.net/dannye/43ud1wvn/
<script>
class FlagIcon extends HTMLElement {
static observedAttributes = ["country"];
log(...args) {
document.body.appendChild(document.createElement("div"))
.innerHTML = `${this.id} - ${args.join` `}`;
}
attributeChangedCallback(name, oldValue, newValue) {
this.log("<b>attributeChangedCallback:</b>", `("${name}" , "${oldValue}", "${newValue}" )`);
if (this.isConnected) {
if (newValue == oldValue) this.log(`Don't call SETTER ${name} again!`);
else this[name] = newValue; // call SETTER
} else this.log("is not a DOM element yet!!!");
}
connectedCallback() {
this.log("<b>connectedCallback</b>, this.img:", this.img || "not defined");
this.img = document.createElement("img");
this.append(this.img); // append isn't available in IE11
this.country = this.getAttribute("country") || "EmptyCountry";
}
get country() { // the Attribute is the truth, no need for private variables
return this.getAttribute("country");
}
set country(v) {
this.log("SETTER country:", v);
// Properties and Attributes are in sync,
// but setAttribute will trigger attributeChanged one more time!
this.setAttribute("country", v);
if (this.img) this.img.src = `//flagcdn.com/20x15/${v}.png`;
else this.log("can't set country", v);
}
}
customElements.define("flag-icon", FlagIcon);
document.body.onclick = () => {
flag1.country = "nl";
flag2.setAttribute("country", "nl");
}
</script>
<flag-icon id="flag1" country="in"></flag-icon><br>
<flag-icon id="flag2" country="us"></flag-icon><br>
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
This is just one way, it all depends on what/when/how your Custom Elements needs to do updates.
It also matters WHEN the CustomElement is defined; before or after the DOM is parsed. Most developers just whack deferred
or method
on their scripts, without understanding what it implies.
Always test your Web Component with code that defines the Custom Element BEFORE it is used in the DOM.
A Real World <flag-icon>
Web Component
Would be optimized:
<script>
customElements.define("flag-icon", class extends HTMLElement {
static observedAttributes = ["country"];
attributeChangedCallback() {
this.isConnected && this.connectedCallback();
}
connectedCallback() {
this.img = this.img || this.appendChild(document.createElement("img"));
this.img.src = `//flagcdn.com/120x90/${this.country}.png`;
}
get country() {
return this.getAttribute("country") || console.error("Missing country attribute",this);
}
set country(v) {
this.setAttribute("country", v);
}
});
</script>
<flag-icon id="flag1" country="gb"></flag-icon>
<flag-icon id="flag2" country="eu"></flag-icon>
<iframe name="sif3" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
Or NO External Images at all
Using the FlagMeister Web Component which creates all SVG client-side
<script src="//flagmeister.github.io/elements.flagmeister.min.js"></script>
<div style="display:grid;grid-template-columns: repeat(3,1fr);gap:1em">
<flag-jollyroger></flag-jollyroger>
<flag-un></flag-un>
<flag-lgbt></flag-lgbt>
</div>
<iframe name="sif4" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>