twitter-embed in LitElement and VanillaJS
Tags: webcomponents, litelement, vanillajs, livecode
Published: 2020-08-26 20:34:23

I've been needing a bit of a pallet cleanse from a work perspective so I dug into our issue queue and found what I figured would be easy: [Embedding twitter conversations #370
](https://github.com/elmsln/issues/issues/370)
Then as I started into it I was like "hmm.. this might make a useful tutorial" so I asked what people wanted it in:

{% twitter 1298632260707639298 %}

Here's the full video walk through, below I'll post code and some things covered

{% youtube aTvtqIEmUuc %}

## Links in the video
- NPM: https://www.npmjs.com/package/@lrnwebcomponents/twitter-embed
- Source: https://github.com/elmsln/lrnwebcomponents/tree/master/elements/twitter-embed
- Iframe sandbox: https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
- Iframe Lazy load: https://web.dev/iframe-lazy-loading/

## VanillaJS
Here's the code for the Vanilla version, it's 100ish lines

```js
class TwitterEmbed extends HTMLElement {
  static get tag() {
    return "twitter-embed";
  }
  /**
   * HTMLElement spec / class based architecture in general
   */
  constructor() {
    super();
    this.dataWidth = this.getAttribute("data-width")
      ? this.getAttribute("data-width")
      : "550px";
    this.dataTheme = this.getAttribute("data-theme")
      ? this.getAttribute("data-theme")
      : "light";
    this.tweet = this.getAttribute("tweet") ? this.getAttribute("tweet") : null;
    this.tweetId = this.getAttribute("tweet-id")
      ? this.getAttribute("tweet-id")
      : null;
    this.allowPopups = this.getAttribute("no-popups") ? "" : "allow-popups";
  }
  /**
   * HTMLElement spec
   */
  static get observedAttributes() {
    return ["tweet", "data-width", "data-theme", "tweet-id", "no-popups"];
  }
  /**
   * HTMLElement spec
   */
  attributeChangedCallback(attr, oldValue, newValue) {
    if (attr == "tweet" && newValue && newValue.includes("twitter.com")) {
      this.tweetId = newValue.split("/").pop();
    }
    if (attr == "no-popups") {
      this.allowPopups =
        newValue == "no-popups" ||
        newValue == "" ||
        !newValue ||
        newValue == null ||
        newValue == "null"
          ? ""
          : "allow-popups";
    }
    if (["no-popups", "tweet-id", "data-width", "data-theme"].includes(attr)) {
      this.innerHTML = this.html;
    }
  }
  get dataWidth() {
    return this.getAttribute("data-width");
  }
  set dataWidth(value) {
    if (value == null || !value) {
      this.removeAttribute("data-width");
    } else {
      this.setAttribute("data-width", value);
    }
  }
  get dataTheme() {
    return this.getAttribute("data-theme");
  }
  set dataTheme(value) {
    if (!value || !["dark", "light"].includes(value)) {
      this.dataTheme = "light";
    } else {
      this.setAttribute("data-theme", value);
    }
  }
  get tweetId() {
    return this.getAttribute("tweet-id");
  }
  set tweetId(value) {
    if (value == null) {
      this.removeAttribute("tweet-id");
    } else {
      this.setAttribute("tweet-id", value);
    }
  }
  get tweet() {
    return this.getAttribute("tweet");
  }
  set tweet(value) {
    if (value == null) {
      this.removeAttribute("tweet");
    } else {
      this.setAttribute("tweet", value);
    }
  }
  /**
   * my own convention, easy to remember
   */
  get html() {
    return `
    <div
      class="twitter-tweet twitter-tweet-rendered"
      style="display: flex; max-width: ${
        this.dataWidth
      }; width: 100%; margin-top: 10px; margin-bottom: 10px;">
      <iframe
        sandbox="allow-same-origin allow-scripts ${this.allowPopups}"
        scrolling="no"
        frameborder="0"
        loading="lazy"
        allowtransparency="true"
        allowfullscreen="true"
        style="position: static; visibility: visible; width: ${
          this.dataWidth
        }; height: 498px; display: block; flex-grow: 1;"
        title="Twitter Tweet"
        src="https://platform.twitter.com/embed/index.html?dnt=true&amp&amp;frame=false&amp;hideCard=false&amp;hideThread=false&amp;id=${
          this.tweetId
        }&amp;lang=en&amp;origin=http%3A%2F%2Flocalhost%3A8000%2Felements%2Ftwitter-embed%2Fdemo%2Findex.html&amp;theme=${
      this.dataTheme
    }&amp;widgetsVersion=223fc1c4%3A1596143124634&amp;width=${this.dataWidth}"
        data-tweet-id="${this.tweetId}">
      </iframe>
    </div>`;
  }
}
customElements.define(TwitterEmbed.tag, TwitterEmbed);
export { TwitterEmbed };
```
### Pros
- 0 dependencies
- platform level code only, all valid conventions
- should theoretically work forever and in any platform

### Cons
- verbose to get data binding to be what's expected
- some weird constructor things / default value stuff
- re-renders are heavy handed

## LitElement
LitElement is a popular
```js
import { LitElement, html } from "lit-element/lit-element.js";
class TwitterEmbedLit extends LitElement {
  static get tag() {
    return "twitter-embed-lit";
  }
  /**
   * HTMLElement spec
   */
  constructor() {
    super();
    this.dataWidth = "550px";
    this.dataTheme = "light";
    this.tweet = null;
    this.tweetId = null;
    this.allowPopups = "allow-popups";
  }
  /**
   * LitElement properties definition
   */
  static get properties() {
    return {
      tweet: {
        type: String
      },
      dataWidth: {
        type: String,
        attribute: "data-width"
      },
      dataTheme: {
        type: String,
        attribute: "data-theme"
      },
      tweetId: {
        type: String,
        attribute: "tweet-id"
      },
      noPopups: {
        type: Boolean,
        attribute: "no-popups"
      },
      allowPopups: {
        type: String
      }
    };
  }
  /**
   * LitElement equivalent of attributeChangedCallback
   */
  updated(changedProperties) {
    changedProperties.forEach((oldValue, propName) => {
      if (propName === "noPopups") {
        if (this[propName]) {
          this.allowPopups = "";
        } else {
          this.allowPopups = "allow-popups";
        }
      }
      if (
        propName === "tweet" &&
        this[propName] &&
        this[propName].includes("twitter.com")
      ) {
        this.tweetId = this[propName].split("/").pop();
      }
    });
  }
  /**
   * Popular convention / LitElement
   */
  render() {
    return html`
      <div
        class="twitter-tweet twitter-tweet-rendered"
        style="display: flex; max-width: ${this
          .dataWidth}; width: 100%; margin-top: 10px; margin-bottom: 10px;"
      >
        <iframe
          sandbox="allow-same-origin allow-scripts ${this.allowPopups}"
          scrolling="no"
          frameborder="0"
          loading="lazy"
          allowtransparency="true"
          allowfullscreen
          style="position: static; visibility: visible; width: ${this
            .dataWidth}; height: 498px; display: block; flex-grow: 1;"
          title="Twitter Tweet"
          src="https://platform.twitter.com/embed/index.html?dnt=true&amp;frame=false&amp;hideCard=false&amp;hideThread=false&amp;id=${this
            .tweetId}&amp;lang=en&amp;origin=http%3A%2F%2Flocalhost%3A8000%2Felements%2Ftwitter-embed%2Fdemo%2Findex.html&amp;theme=${this
            .dataTheme}&amp;widgetsVersion=223fc1c4%3A1596143124634&amp;width=${this
            .dataWidth}"
          data-tweet-id="${this.tweetId}"
        >
        </iframe>
      </div>
    `;
  }
}

customElements.define(TwitterEmbedLit.tag, TwitterEmbedLit);
export { TwitterEmbedLit };
```
### Cons
- 1 dependency
- Some library specific conventions

### Pros
- conventions are simple
- lit-html template rewriting conventions may become platform level code in the future
- tidy code, slightly less to write, easier to read
- faster re-render, though very small to see in this example