Home > other >  What is the minimal implementation for custom elements example mentioned in the specifications?
What is the minimal implementation for custom elements example mentioned in the specifications?

Time:10-18

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>

  • Related