Context-Aware Web Components Are Easier Than You Think | CSS-Tricks

Another aspect of web components that we haven’t talked about yet is that a JavaScript function is called whenever a web component is added or removed from the page. These lifecycle callbacks can be used for many things, including making an object aware of its context.

Article series

The four lifecycle callbacks for web components

There are four lifecycle invocations that can be used with web components:

  • connectedCallback: This callback is activated when the custom item is attached for the element.
  • disconnectedCallback: This callback is triggered when the item is Removal from the document.
  • adoptedCallback: This callback is triggered when the item is added to a new document.
  • attributeChangedCallback: This callback is fired when a file attributed They are changed, added or removed, as long as this attribute is observed.

Let’s take a look at each of these in action.

Our personal component after the end of the world

Two web component views side by side, the left is human, and the right is a zombie.

We’ll start by creating a web component called <postapocalyptic-person>. Everyone after the apocalypse is either a human or a zombie and we’ll know which one depends on which class – either .human or .zombie – This is applied to the main element of <postapocalyptic-person> component. We won’t do anything fancy with it (yet), but we will add a file shadowRoot We can use it to attach a corresponding photo based on this rating.

customElements.define(
  "postapocalyptic-person",
  class extends HTMLElement {
    constructor() {
      super();
      const shadowRoot = this.attachShadow({ mode: "open" });
    }
}

Our HTML looks like this:

<div class="humans">
  <postapocalyptic-person></postapocalyptic-person>
</div>
<div class="zombies">
  <postapocalyptic-person></postapocalyptic-person>
</div>

Input people using connectedCallback

when <postapocalyptic-person> On the page, the file connectedCallback() The function is called.

connectedCallback() {
  let image = document.createElement("img");
  if (this.parentNode.classList.contains("humans")) {
    image.src = "https://assets.codepen.io/1804713/lady.png";
    this.shadowRoot.appendChild(image);
  } else if (this.parentNode.classList.contains("zombies")) {
    image.src = "https://assets.codepen.io/1804713/ladyz.png";
    this.shadowRoot.appendChild(image);
  }
}

This makes sure the human image is output when it is a file <postapocalyptic-person> It is a human image, and a zombie image when the component is a zombie.

Be careful working with connectedCallback. It plays more than you might realize, as it fires any time an item is moved and can (confusingly) even trigger it. after The node is no longer connected – which can represent a prohibitive performance cost. you can use this.isConnected To see if the item is online or not.

Count people with connectedCallback() When they are added

Let’s make it more complicated by adding a few buttons to the mix. One will add a <postapocalyptic-person>, using a “coin flip” approach to determine if it’s a human or a zombie. The other button will do the opposite, removing a file <postapocalyptic-person> at random. We’ll keep track of how many humans and zombies are in the show while we’re at it.

<div class="btns">
  <button id="addbtn">Add Person</button>
  <button id="rmvbtn">Remove Person</button> 
  <span class="counts">
    Humans: <span id="human-count">0</span> 
    Zombies: <span id="zombie-count">0</span>
  </span>
</div>

Here’s what our buttons will do:

let zombienest = document.querySelector(".zombies"),
  humancamp = document.querySelector(".humans");

document.getElementById("addbtn").addEventListener("click", function () {
  // Flips a "coin" and adds either a zombie or a human
  if (Math.random() > 0.5) {
    zombienest.appendChild(document.createElement("postapocalyptic-person"));
  } else {
    humancamp.appendChild(document.createElement("postapocalyptic-person"));
  }
});
document.getElementById("rmvbtn").addEventListener("click", function () {
  // Flips a "coin" and removes either a zombie or a human
  // A console message is logged if no more are available to remove.
  if (Math.random() > 0.5) {
    if (zombienest.lastElementChild) {
      zombienest.lastElementChild.remove();
    } else {
      console.log("No more zombies to remove");
    }
  } else {
    if (humancamp.lastElementChild) {
      humancamp.lastElementChild.remove();
    } else {
      console.log("No more humans to remove");
    }
  }
});

This is the code in connectedCallback() Counts humans and zombies when added:

connectedCallback() {
  let image = document.createElement("img");
  if (this.parentNode.classList.contains("humans")) {
    image.src = "https://assets.codepen.io/1804713/lady.png";
    this.shadowRoot.appendChild(image);
    // Get the existing human count.
    let humancount = document.getElementById("human-count");
    // Increment it
    humancount.innerHTML = parseInt(humancount.textContent) + 1;
  } else if (this.parentNode.classList.contains("zombies")) {
    image.src = "https://assets.codepen.io/1804713/ladyz.png";
    this.shadowRoot.appendChild(image);
    // Get the existing zombie count.
    let zombiecount = document.getElementById("zombie-count");
    // Increment it
    zombiecount.innerHTML = parseInt(zombiecount.textContent) + 1;
  }
}

Update counts with disconnectedCallback

Then, we can use disconnectedCallback() To reduce the number where humans and zombies are removed. However, we are unable to check the class of the parent item because the parent item with the corresponding class has already disappeared over time disconnectedCallback he is called. We can set an attribute on the element, or add a property to the object, but since the image src The theme is already defined by its parent element, we can use that as a proxy to see if the web component being removed is a human or a zombie.

disconnectedCallback() {
  let image = this.shadowRoot.querySelector('img');
  // Test for the human image
  if (image.src == "https://assets.codepen.io/1804713/lady.png") {
    let humancount = document.getElementById("human-count");
    humancount.innerHTML = parseInt(humancount.textContent) - 1; // Decrement count
  // Test for the zombie image
  } else if (image.src == "https://assets.codepen.io/1804713/ladyz.png") {
    let zombiecount = document.getElementById("zombie-count");
    zombiecount.innerHTML = parseInt(zombiecount.textContent) - 1; // Decrement count
  }
}

Beware of clowns!

Now (and I’m speaking from experience here, of course) the only thing more terrifying than a horde of zombies clicking on your site is a clown – all it takes is one! So, even though we’re already dealing with scary zombies after the apocalypse, let’s add the possibility of a clown entering the scene for even more horror. In fact, we will do it in such a way that it is possible for any human or zombie on screen to be a clown in secret!

I take back what I said earlier: a single zombie clown is terrifying even from a group of “normal” clowns. Suppose that if any kind of clown is found – be it human or zombie – we separate them from humans and groups of zombies by sending them to a completely different document – <iframe> Prison, if you will. (I’ve heard that “clown” may be more contagious than a zombie.)

And when we move a suspected clown from the current document to a file <iframe>, do not destroy and recreate the original node; Rather, it depends and ties the knot mentioned first adoptedCallback and then connectedCallback.

We don’t need anything in <iframe> Document excluding body with extension .clowns Class. As long as this document is in the iframe of the main document – not displayed separately – we don’t even need a file <postapocalyptic-person> Instance creation icon. We’ll include one space for humans, another space for zombies, and yes, a prison for clowns… Err… <iframe> of fun.

<div class="btns">
  <button id="addbtn">Add Person</button>
  <button id="jailbtn">Jail Potential Clown</button>
</div>
<div class="humans">
  <postapocalyptic-person></postapocalyptic-person>
</div>
<div class="zombies">
  <postapocalyptic-person></postapocalyptic-person>
</div>
<iframe class="clowniframeoffun” src="https://css-tricks.com/context-aware-web-components/adoptedCallback-iframe.html">
</iframe>

The Add Person button works the same way it did in the last example: it flips a cryptocurrency to randomly insert either a human or a zombie. When we press the “Jail Potential Clown” button, another coin is flipped and takes either a zombie or a human, and delivers it to the <iframe> prison.

document.getElementById("jailbtn").addEventListener("click", function () {
  if (Math.random() > 0.5) {
    let human = humancamp.querySelector('postapocalyptic-person');
    if (human) {
      clowncollege.contentDocument.querySelector('body').appendChild(document.adoptNode(human));
    } else {
      console.log("No more potential clowns at the human camp");
    }
  } else {
    let zombie = zombienest.querySelector('postapocalyptic-person');
    if (zombie) {
      clowncollege.contentDocument.querySelector('body').appendChild(document.adoptNode(zombie));
    } else {
      console.log("No more potential clowns at the zombie nest");
    }
  }
});

Detect clowns with adoptedCallback

In the adoptedCallback We will determine if the clown is a human zombie type based on the corresponding image and then change the image accordingly. connectedCallback It will then be called, but we don’t have anything that needs to be done, and what it does won’t conflict with our changes. So we can leave it as is.

adoptedCallback() {
  let image = this.shadowRoot.querySelector("img");
  if (this.parentNode.dataset.type == "clowns") {
    if (image.src.indexOf("lady.png") != -1) { 
      // Sometimes, the full URL path including the domain is saved in `image.src`.
      // Using `indexOf` allows us to skip the unnecessary bits. 
      image.src = "https://css-tricks.com/context-aware-web-components/ladyc.png";
      this.shadowRoot.appendChild(image);
    } else if (image.src.indexOf("ladyz.png") != -1) {
      image.src = "ladyzc.png";
      this.shadowRoot.appendChild(image);
    }
  }
}

Hidden clowns revealed by attributeChangedCallback

Finally, we have a file attributeChangedCallback. Unlike the other three lifecycle callbacks, we need to note the attributes of our web component in order for the callback to be activated. We can do this by adding observedAttributes() function to the custom element class and make that function return an array of attribute names.

static get observedAttributes() {
  return [“attribute-name”];
}

Then, if this attribute changes – including adding or removing – then attributeChangedCallback fires.

Now, the thing to worry about with clowns is that some of the humans you know and love (or those you knew and loved before they turned into zombies) could be secretly clowns in disguise. You’ve set up a clown detector that searches a bunch of humans and zombies, and when you click the “clown detector” button, the detector will (by totally scientific and totally trustworthy means) Not Based on random numbers you choose an index) applies data-clown="true" for the component. And when this feature is applied, attributeChangedCallback It triggers and updates the component image to reveal its clown colors.

I should also point out that attributeChangedCallback It takes three parameters:

  • Attribute name
  • The previous value of the attribute
  • The new value of the attribute

Furthermore, the callback allows you to make changes based on how much the theme has changed, or based on the transition between two states.

here we have attributeChangedCallback Code:

attributeChangedCallback(name, oldValue, newValue) {
  let image = this.shadowRoot.querySelector("img");
  // Ensures that `data-clown` was the attribute that changed,
  // that its value is true, and that it had an image in its `shadowRoot`
  if (name="data-clown" && this.dataset.clown && image) {
    // Setting and updating the counts of humans, zombies,
    // and clowns on the page
    let clowncount = document.getElementById("clown-count"),
    humancount = document.getElementById("human-count"),
    zombiecount = document.getElementById("zombie-count");
    if (image.src.indexOf("lady.png") != -1) {
      image.src = "https://assets.codepen.io/1804713/ladyc.png";
      this.shadowRoot.appendChild(image);
      // Update counts
      clowncount.innerHTML = parseInt(clowncount.textContent) + 1;
      humancount.innerHTML = parseInt(humancount.textContent) - 1;
    } else if (image.src.indexOf("ladyz.png") != -1) {
      image.src = "https://assets.codepen.io/1804713/ladyzc.png";
      this.shadowRoot.appendChild(image);
      // Update counts
      clowncount.innerHTML = parseInt(clowncount.textContent) + 1;
      zombiecount.innerHTML = parseInt(zombiecount.textContent) - 1;
    }
  }
}

And there you have it! Not only have we discovered that reconnecting web components and creating context-aware custom items is easier than you thought, but spotting post-apocalyptic clowns, while terrifying, is also easier than you thought. What kind of post-apocalyptic scammer can you spot using the web component’s callback functions?

Leave a Comment