Chapter 4. The component lifecycle

This chapter covers

  • Using the connectedCallback Web Components API method to listen when your component is added to the DOM
  • Knowing when and how to use the constructor method, especially because it occurs before the component has access to the DOM
  • Utilizing the disconnectedCallback Web Components API method to clean up after your component
  • The seldom-used adoptedCallback Web Components API method

4.1. The Web Components API

Up to now, we’ve explored a couple different methods from the Web Components API, but we really didn’t talk about the API as a whole. These methods are the basic building blocks for building everything from custom components to entire applications. So, it’s a good idea to take a look at all of them in detail. In the last chapter, we looked at the attributeChangedCallback and the observedAttributes static getter. In this chapter, we’ll cover the rest in the same amount of detail.

Additionally, we need to consider that now that Web Components are shipping in browsers, the specification should be considered a permanent part of the web development workflow for years to come. With this in mind, we should have some confidence that Web Components can be used in a variety of situations.

The most obvious use case for Web Components intersects with those use cases that big frameworks such as Angular, React, and Vue are targeting. Generally speaking, this use case is a data-centric web application that might interact with a REST-based API. On the other side of the spectrum, as we see more graphic-intensive uses for the web, like games, 3D, video, and so on, we need to know that the Web Components API can handle those too.

To have this confidence, I want to cover the entire API in detail but also compare it to a couple different component lifecycles. For more traditional web applications, we can look at a typical React component lifecycle. For more graphic-intensive applications, we can look at the component lifecycle for an extremely successful 3D/game engine (not web-based) called Unity.

4.2. The connectedCallback handler

We’ve previously tapped into the connectedCallback method in examples from the last couple of chapters, but let’s revisit it. This time, however, let’s add back an alert inside a generic component to alert us exactly when our component starts up.

Listing 4.1. Testing when our connectedCallback is called
<script>
   class MyCustomTag extends HTMLElement {
       connectedCallback() {
           alert('hi from MyCustomTag');                            1
           this.innerHTML = '<h2>'+ this.getAttribute('title') +
             '</h2><button>click me</button>';
       }
   }

   if (!customElements.get('my-custom-tag')) {
       customElements.define('my-custom-tag', MyCustomTag);
   }
</script>

<style>
   my-custom-tag {
       background-color: blue;
       padding: 20px;
       display: inline-block;
       color: white;
   }
</style>

<body>
<my-custom-tag title="Another title"></my-custom-tag>
</body>

  • 1 Alert added to our previous example’s connectedCallback

Of course, what we should see when running this code in our browser is even more basic than what we had in the last couple of chapters: a simple, ugly Web Component with a header and a button that says “click me.” With the alert added back in, you’ll also see a modal box pop up immediately that says “hi from MyCustomTag.”

The question now, based on the limited amount of code we have here, is when does connectedCallback get called? The name of this method is a clue, but let’s explore by removing the <my-custom-tag title="Another title"></my-custom-tag> from the body of our page.

Now, visually we have a completely empty page, but we’re still doing things on this page. Our script block is still running, so we’re still registering this custom component as something we could use. We’re just not putting it on the page yet.

With this in mind, and our element removed from the body, let’s refresh the page: no element, and no alert. Let’s use our component’s constructor to poke at this a bit more. If you recall from chapter 2, we identified the constructor as the function that runs when the class is instantiated.

Note that because we’re using a constructor in an inherited class, we must call super(); as the first line. By doing this, HTMLElement’s constructor is called as well. Usually, when calling the inherited method, you might call super.myInheritedMethod() on any line, but here in the constructor, it’s just super(); on the first line in the constructor.

Listing 4.2. Alerting from both our constructor and our connectedCallback
<script>
   class MyCustomTag extends HTMLElement {
       constructor()  {
              super();
              alert('hi from MyCustomTags      1
               constructor');
       }
       connectedCallback() {
           alert('hi from MyCustomTag          2
           connected callback');
           this.innerHTML = '<h2>'+ this.getAttribute('title') +
             '</h2><button>click me</button>';
       }
   }

   if (!customElements.get('my-custom-tag')) {
       customElements.define('my-custom-tag', MyCustomTag);
   }
</script>

  • 1 Alert added to constructor
  • 2 Alert remaining in connectedCallback to compare timing

OK, so if we refresh this page . . . well, nothing happens—again. Note that while we fully defined our element, we haven’t instantiated or called it into action yet! To test our theory that the constructor is called on creation, and connectedCallback happens when added to the DOM, let’s do a bit of manual DOM manipulation with JS.

With the blank page loaded, we’ll open up the browser dev tools and open the console. In the console, enter

x = document.createElement('my-custom-tag');

Great! Our constructor alert is fired, and we see the message “hi from MyCustomTags constructor.” By creating the element, we’ve implicitly called new MyCustomTag(); and, as a result, the constructor is called. At the same time, however, the connectedCallback method has not been called because we haven’t added it to our DOM. Let’s do that now! In the same console, now that our x variable is set, run the following:

document.body.appendChild(x);

As expected, the alert from the connectedCallback is called. Also, you should now see the component in the page’s body. This flow, from creation to connectedCallback, is captured in figure 4.1.

Figure 4.1. The start of a Web Component’s lifecycle: constructor first, and then connectedCallback after adding to the DOM

What if we tried something a little more indirect? What we just did begs the question of whether connectedCallback was fired because we added it to any element or if it was a matter of adding it to our page’s DOM. Let’s test this by refreshing the page and creating our element again in the console:

myEl = document.createElement('my-custom-tag');

Of course, the constructor alert will still fire and show us the message. Next, let’s create yet another element to act as a container:

myContainer = document.createElement('div');

Now comes the moment of truth. Will our connectedCallback alert us when we add myEl to myContainer? Let’s try:

myContainer.appendChild(myEl);

And the answer is no! Adding the custom component to just any element not yet attached to the DOM will not trigger the connectedCallback method. We have an isolated node held in the myContainer variable. The node looks like this:

<div>
       <my-custom-tag></my-custom-tag>
</div>

Although we’ve proven that our connectedCallback method is not fired when adding it to something that’s not connected to the DOM, we haven’t yet proven that indirectly adding to the DOM will fire that method. Let’s continue in the console and try:

document.body.appendChild(myContainer);

Confirmed! Instead of adding our custom element directly to the page, we’ve first added it to another container (a <div>). We then added that container to our DOM, and our connectedCallback method is still called, proving that the callback is called only when it’s added to the page and nowhere else, even if not directly added to the page.

Additionally, if we remove the element and then re-add it, we see that our connectedCallback is called each time:

document.body.removeChild(myContainer);
document.body.appendChild(myContainer);

This actually means that if you add, remove, and then add your component again, you should be careful to do any one-time setup you intend only once.

Figure 4.2 recaps our explanation with four scenarios. A component can be directly on the page, or even inside another component. If either the component or the outer component (and it could be the outer, outer, outer component) is on the main HTML page, the connectedCallback will be called.

Figure 4.2. Four different scenarios for creating your Web Component

Alternately, even if the component is added inside another element, its connectedCallback won’t be fired if the outer element is not on the main page. Generally speaking, for that connectedCallback to fire, the component must have an ancestor on the main HTML page.

4.2.1. Constructor vs. connected

What does this all mean for practical purposes? What logic belongs in the constructor versus the connectedCallback method? It would be reasonable to think that we can shove everything into the constructor and keep the connectedCallback method empty. Unfortunately, no—there is a bit of nuance here.

A big aspect of what you’ll want to do when creating a component is to set the content of your element. You’ll likely want to set innerHTML to some markup. It’s how, in our simple example, we’re adding the header and button. You might also want to get an attribute of your component. Unfortunately, when the constructor is fired, the element isn’t yet ready to be interacted with in this way.

We can prove this by moving the innerHTML line to the constructor, as follows.

Listing 4.3. Trying (and failing) to set innerHTML from the constructor
class MyCustomTag extends HTMLElement {
   constructor()  {
       super();
       this.innerHTML = '<h2>'+ this.getAttribute('title') +
         '</h2><button>click me</button>';
   }
   connectedCallback() {}
}

When our page reloads, we can try creating the element again with the createElement function, but the following error is seen in our console:

DOMException: Failed to construct 'CustomElement': The result must not have
     children

Our browser is telling us that when our custom element is initially created, it’s not allowed to have children. Furthermore, we can check on our title attribute that we’ve been using to populate our header tag in the constructor versus the connectedCallback.

Listing 4.4. Attempting to access attributes on the constructor vs. connectedCallback
class MyCustomTag extends HTMLElement {
   constructor()  {
       super();
       console.log('From constructor',                1
                    this.getAttribute('title'));
   }
   connectedCallback() {
       console.log('From connectedCallback',
                    this.getAttribute('title'));      2
   }
}

  • 1 Accessing an attribute on this component from the constructor (failed)
  • 2 Accessing an attribute on this component from the connectedCallback (success)

When we change to the previous listing and reload our page, our console will indicate that the constructor doesn’t know the title yet, logging null. Our connectedCallback is just fine, though.

Just by looking at what works and what doesn’t here, we can start to feel out how we should organize our component. The connectedCallback should contain all the logic to populate our element visually. For a typical component, lots of logic within, like adding events, interactions, and so on, will depend on these visuals being present. This can leave the constructor fairly empty or devoid of meaningful code for many situations.

Depending on your component, however, there are likely to be exceptions that should live in the constructor. One such exception is logic that you may want to happen after your element is initialized, but prior to it being added to the page. You may want, for example, to create the element in advance and do a network request to pull information off the internet before you append your component to the DOM. In this fashion, if your component has all the data it needs to render, it can do so instantly when on the page. In this case, because there are no dependencies on the visual elements within your component, the constructor can be a good place for this code.

Listing 4.5. A nicely formatted property list in a constructor
class MyCustomTag extends HTMLElement {
   constructor()  {                             1
          super();
   /**
    * URL to fetch data to populate our hypothetical list
    */
   this.serviceURL =                            2
'http://company.com/service.json';

   /**
    * internal counter to track something
    */
   this.counter = 0;

   /**
    * last error message displayed
    */
   this.error;
   }
   connectedCallback() { . . . }
}

  • 1 Constructor method
  • 2 Adds human-readable properties to the constructor

As I mentioned at the start of the chapter, one great use of the constructor can be to contain property declarations. It’s really handy to have a constructor at the top of your class and be able to easily read all the properties that you use within, as seen in listing 4.5. I’ve found that even if you don’t set your properties to anything yet, it’s still great for component readability. I should mention again, however, that with the latest version of Chrome supporting public and private class fields, we can declare our properties in the class itself, which is nicer and more inline with every other language that supports classes. Once other browsers pick up support, the approach I just outlined will likely be something of a bad practice.

One big caveat to using the constructor versus the connectedCallback for DOM-related logic arises if you are using the Shadow DOM, which will come up in chapter 7. When using the Shadow DOM, you’re creating a separate mini DOM that’s internal to your component. In this case, the Shadow DOM is available whenever you create it—even in the constructor.

This caveat is why you’ll see many modern Web Components use the constructor for most everything in the component, while the connectedCallback might not be used much at all.

Will you use the Shadow DOM? Up until recently, I wouldn’t have recommended it, but Firefox just shipped an update with support for it (along with all Web Component features), and Edge should ship a release beyond its development preview soon.

As awesome as the Shadow DOM is, you’ll need to weigh whether you need it and whether it’s supported in the browser of your choice. There will certainly be situations where the Shadow DOM just doesn’t make sense for your project—knowing the nuances of the connectedCallback versus constructor methods will be important.

4.3. The remaining Web Component lifecycle methods

We’ve discussed four of the six methods of our component lifecycle (constructor, connectedCallback, attributeChangedCallback, and observedAttributes). There are just two remaining methods: disconnectedCallback and adoptedCallback.

4.3.1. Disconnected callback

The disconnectedCallback serves a very important purpose, which is to give the component an opportunity to clean up after itself. This callback is fired when the component is removed from the DOM.

The reason for cleanup is twofold. First, you don’t want stray code running when you don’t need it. Second is to give garbage collection a chance to run. If you’re not familiar with garbage collection, consider a language like C++. When you store data in a variable, it will never go away, or get released, to use proper terminology. As a developer, it is your job to properly release it when you are done. If you’re not careful, all the variables you’re not using anymore can start adding up and consuming tons of memory! Luckily, with more modern languages like JS, your unused variables will get “garbage-collected.” Every once in a while, when the engine (in our case, the JS engine) knows it has enough idle time to clean up, it will go in and release the variables you aren’t using. It’s not psychic, though, and can’t predict what you don’t need. Instead, if it sees that you don’t reference or link to something in memory, as in figure 4.3, it will release it. This is why the disconnectedCallback is a good opportunity to reset or null any variables that might link to other objects.

Figure 4.3. Memory references inside a Web Component

It can definitely be a chore to worry about these finer details when your component just works. Occasionally, if we know exactly how we are using our component, we can ignore some of this. For example, if you know that your application will never be removed from the DOM, you might be able to ignore cleanup. Of course, the scope of projects can change, and that component you never expected to be removed might need to be.

To cite an example of much-needed cleanup, say you query a server every 30 seconds to get updated data. If you removeChild(yourelement); from its parent container, it will still run that timer and still query the server. Let’s try a simplified experiment using a countdown timer example.

Listing 4.6. A demonstration of code running after the element has been removed
<html>
<head>
   <meta charset="UTF-8">
   <title>Cleanup Component</title>
   <script>
       class CleanupComponent extends HTMLElement {
           connectedCallback() {
               this.counter = 100;
               setInterval( () =>
                   this.update(), 1000);                              1
           }

           update() {
               this.innerHTML = this.counter;
               this.counter --;
               console.log(this.counter);                             2
           }
       }

       customElements.define('cleanup-component', CleanupComponent);
   </script>
</head>

<body>
   <cleanup-component></cleanup-component>
   <button onclick="document.body.removeChild(document.querySelector
                   ('cleanup-component'))">remove</button>            3
</body>
</html>

  • 1 Starts the countdown timer
  • 2 Console logs the current timer value (still running after component is removed!)
  • 3 Button to remove the component

In this example, we’re also logging our counter value with

console.log(this.counter);

I’ve also added a button with some inline JS code. When you click the Remove button, the countdown timer component is removed from the DOM.

When you run the example, the timer counts down as usual. After clicking Remove, you don’t see the timer anymore, but if you open the console log, you’ll see that it’s still counting down! It’s bad enough to leave that timer running—even worse that we’re muddying up the console log with elements we don’t want anymore. It would be still worse if we were making network requests we don’t care about or doing something computationally expensive for an element we don’t need.

So, we can use the disconnectedCallback to clean up our timer. We’ll likely want to clean any event listeners added as well, such as mouse events. Let’s try cleaning up our timer when the element is removed in the following listing.

Listing 4.7. Using disconnectedCallback to clean up a timer
class CleanupComponent extends HTMLElement {
   connectedCallback() {
       this.counter = 100;
       this.timer = setInterval( () => this.update(), 1000);
   }

   update() {
       this.innerHTML = this.counter;
       this.counter --;
       console.log(this.counter);
   }

   disconnectedCallback() {
       clearInterval(this.timer);        1
   }
}

  • 1 When component is removed (on disconnectedCallback), removes the timer

We’ve now captured our timer in a variable:

this.timer = setInterval( () => this.update(), 1000);

This way, when we need to clean up using disconnectedCallback, we can clear it using the same variable:

   disconnectedCallback() {
       clearInterval(this.timer);
   }

Checking our logs again, we have no more messages, and our element should be properly garbage-collected on the next pass.

4.3.2. Adopted callback

Despite the fact that even I need to buckle down and use disconnectedCallback more to write better and more versatile components, this last lifecycle method I truly can’t see most people ever needing. The adoptedCallback lifecycle method fires when your Web Component moves to a different document.

Don’t worry if this doesn’t make sense, because it doesn’t usually happen. Usually, you’ll have only one document per HTML page. The exception to this is when using iframes (or inline-frames), which have really fallen out of favor for most uses. Basically, with an iframe, you have a mini HTML page in a frame on your master HTML page.

Elements can be stolen from the iframe and placed into the surrounding page, or vice versa. To do this, you’d grab a reference to the element and then move it to the new document:

const frame = document.getElementsByTagName("iframe")[0]
const el = frame.contentWindow.document.getElementsByTagName(
       "my-custom-component")[0];
const adopted = document.adoptNode(el);

Once done, the adoptedCallback lifecycle method will fire. But again, on the rare occasion I’ve found myself working with iframes, I’ve never had to move nodes from one document to the other. Maybe you’ll find a use for this method, and if you do, know that your component can listen!

4.4. Comparing to React’s lifecycle

Let’s now talk about the Web Component lifecycle in relation to the React lifecycle. After all, with only a handful of lifecycle methods, it can feel like Web Components might be lacking. Given how popular React is, and its wide audience of developers, it’s great for measuring Web Components against to see how they stack up.

React is a bit opinionated, like all frameworks and libraries tend to be. It offers a specific component lifecycle that works for React developers and their use cases. Of course, there’s absolutely nothing wrong with this, but the point is that we’re looking at a lifecycle that may or may not apply to how you want to work. I’d like to reiterate that this is exactly what I love about working with Web Components—they have just enough features to cover the bare minimum of what you need, and anything beyond that can be built up with your own code or existing microframeworks or libraries.

The React documentation breaks down its lifecycle methods into four main categories: mounting, updating, unmounting, and error handling. The error-handling method is one we haven’t gotten into yet, and indeed, there is nothing similar in Web Components. React’s philosophy here (at least as of v16) is to establish “error boundaries” such that if you have an error in one component, it doesn’t take the rest of your components or the application down with it.

While it is true that a JS error has the potential to do some really bad and unexpected things anywhere in a Web Components-based application, with React, it was a little worse. Prior to v16, an error promised to unmount your entire application! Errors in vanilla JS are usually tamer—unexpected things will happen, but usually your application won’t be brought to its knees. As a result, in v16, React created error boundaries so that each component could handle any badness and not affect the rest. Web Components are a little more decentralized, so React’s problems aren’t so similar.

In React, mounting means creating a chunk of HTML that represents your component and then inserting that HTML into the DOM. For mounting, there are several relevant methods.

Like Web Components (and most everything else), React lets you override the constructor. The types of things you’d do are very similar to Web Components, in that you’d likely not want to put tons of component logic here, and you’d ideally initialize things that you’d use later. The methods componentWillMount and componentDidMount let you do stuff before and after the component is added to the DOM.

While componentDidMount is a lot like Web Components’ connectedCallback, there doesn’t seem to be lots of purpose for componentWillMount. There’s nothing here you couldn’t just do with the constructor. In fact, React v16 is already showing warning messages that this method will be deprecated in the next major version.

Prior to componentDidMount (or when the component changes in some way), you are allowed to override the render method. With this method, you would mainly return HTML to represent your component’s inner markup.

With Web Components, render just isn’t necessary as a standard lifecycle method, though LitElement and others have added this to their Web Components to make updating HTML more streamlined. With the basic lifecycle as is, we can control our component’s innerHTML at any time and aren’t limited by our component lifecycle for when to set our component’s contents, or even which pieces are updated. In this regard, we are better off being unbound by stiff rules that say where or when we can create the inner workings of our component! With LitElement and various frameworks, you’re buying into a design pattern and making the choice to be bound by some rules that dictate when your component renders. Great, if that’s what you choose, but as a standard that needs to fit a variety of use cases, I think it’s much better to opt-in to something like a render method.

For updating the component, React has several methods as well: componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, getSnapshotBefore Update, and componentDidUpdate. In addition to componentWillReceiveProps being deprecated soon, the rest are helpers for when something changes in your component, and it needs to update. They are less relevant to Web Components because React, as a system, keeps track of a bunch of stuff outside the scope of your actual HTML element. State, properties, and so on are all things that change and trigger your component to change. In fact, React has a different suggested usage altogether. You are supposed to change state or properties, and your component is supposed to . . . well . . . “react” to these changes.

When you interact with Web Components, on the other hand, you’ll likely do so much like you’d interact with a normal DOM element: through a custom API or using attributes. With this difference, the need for these extra methods melts away. Some might argue that the way React works offers more of a helping hand, but with Web Components, you have more freedom to do things how you want, specific to your own project.

4.5. Comparing to a game engine lifecycle

Speaking of freedom to implement how we want depending on the project, we shouldn’t regard traditional web applications as the only use case for building something on the web. More and more graphics-intensive projects are being built all the time. A good use case to consider is a game engine. In this regard, I think it’s fair to compare the Web Component lifecycle to Unity. Unity 3D is one of the most popular tools for making real-time 3D for games, applications, and even AR/VR.

In Unity, a developer typically works with a 3D object of some sort that has a Monobehavior attached. Much like our Web Component extends HTMLElement, a custom Unity behavior extends Monobehavior.

Monobehavior has two lifecycle methods used for starting a behavior. Awake is like our Web Component constructor. It gets called when the Monobehavior is created, regardless of whether it’s enabled or not. With Unity, behaviors aren’t necessarily active and running if they are disabled.

Likewise, our Web Component isn’t really “enabled” if it hasn’t been added to the DOM, because it’s not visually on the page. Unity has OnEnable and OnDisable methods to watch for this. A behavior can get enabled multiple times, just like our Web Component can get added to the DOM multiple times. So here, OnEnable is a lot like our Web Component’s connectedCallback.

Unity’s Start method gets called the first time the behavior is enabled, including if it’s enabled when the application starts. Web Components don’t have a similar call, and like I said, if we add the same element to our DOM more than once, we need to guard against any re-initialization if it hurts our components. Luckily, this is easy to overcome—we can just set a variable to true the first time going through our connectedCallback and avoid calling the same initialization with an if/then.

These subtle distinctions only matter if you choose to not use your Web Component in the simple way of just writing markup in your HTML, as in when creating, adding, and removing elements with JS. For example, when prototyping or building a specific application, you’ll probably know exactly how your Web Components are to be used and be able to adjust as needed. If you’re building a library of Web Components you intend to share, you may want to consider all of these use cases.

Next, Unity 3D has several methods in its Monobehavior lifecycle that are called each render frame, which means they are called many times per second to give the developer an opportunity to update what gets drawn on screen when graphics are updated. These methods handle specific things like physics, different render passes, and so on. For our purposes, I’ll condense them down to Unity’s update method because unless we get into WebGL or other specific cases, they really don’t apply to Web Components.

While Web Components don’t have a similar update method as part of the lifecycle API, or even the variety of update methods I’ve described previously, we arguably don’t need one. We aren’t necessarily doing games or graphics-intensive things that need to run every frame with JS, so in those cases, we don’t need it. On the occasion we do need an update method, there are a couple of ways we can do it.

The first thing we can try is a timer. Let’s take that timer example we had before, and start there.

Listing 4.8. A countdown timer component
<html>
<head>
   <meta charset="UTF-8">
   <title>Countdown Timer</title>
   <script>
       class CountdownTimer extends HTMLElement {
           connectedCallback() {
               this.counter = 100;
               setInterval( () =>                   1
                      this.update(), 1000);
           }

           update() {
               this.innerHTML = this.counter;       2
               this.counter --;                     3
           }
       }

       customElements.define('countdown-timer', CountdownTimer);
   </script>
</head>

<body>
   <countdown-timer></countdown-timer>
</body>
</html>

  • 1 Creates our internal timer (calls update every second)
  • 2 Displays the timer’s current value
  • 3 Decrements every timer update

In listing 4.8, we’ve created a simple example countdown timer component (virtually the same as earlier in this chapter). When our component is added to the DOM, we use our connectedCallback to initialize a property called counter and set it to 100. We also start a standard JS timer and attach that to an internal method called update:

setInterval( () => this.update(), 1000);

If you have used the timer before, you know the last parameter of 1,000 makes the timer fire every 1,000 milliseconds (or every second). On the Update method itself, we simply set the contents of our component with innerHTML and decrement our variable by one.

What you’ll see in your browser when you run this is a numeric display that starts at 100 and counts down by 1 every second. setInterval is great for situations like this where you just need a normal timer; but for animation or graphics that need to change every 1/30th of a second, for example, JS’s newer requestAnimationFrame will produce smoother results that are actually tied to the browser’s render cycle.

Let’s swap our setInterval for requestAnimationFrame and do something a little more animated in the next listing.

Listing 4.9. Swapping setInterval for requestAnimationFrame
<html>
<head>
    <title>Visual Countdown Timer</title>
    <script>
        class VisualCountdownTimer extends HTMLElement {
            connectedCallback() {
                this.timer = 200;
                this.style.backgroundColor = 'green';
                this.style.display = 'inline-block';
                this.style.height = '50px';
                requestAnimationFrame( () =>        1
                       this.update());
            }

            update() {
                this.timer --;
                if (this.timer <= 0) {
                    this.timer = 200;
                }
                this.style.width =                  2
                       this.timer + 'px';
                requestAnimationFrame( () =>        3
                       this.update());
        }

        customElements.define('countdown-timer', VisualCountdownTimer);
    </script>
</head>
<body>
    <countdown-timer></countdown-timer>
</body>
</html>

  • 1 Using requestAnimationFrame instead of setInterval
  • 2 Smoothly animates the width of our component
  • 3 Keeps requestAnimationFrame going by calling it every update

With the exception of requestAnimationFrame happening only once, thereby forcing us to call it on every update call, the implementation is mostly the same as setInterval:

requestAnimationFrame( () => this.update());

Again, I have a counter, but I call it timer now, because we’ll be making our component shrink with each animation frame to simulate a countdown timer. I also have some CSS styling to set the background color, height, and inline-block style of the component. It’s not awesome that I’m setting style with code here when I could use CSS, but I want to keep this example dead simple:

this.style.backgroundColor = 'green';
this.style.display = 'inline-block';
this.style.height = '50px';

On the update method, we decrement our timer and also check if it’s equal to or smaller than 0. If so, then we reset it to 200, just to keep our component in an infinitely demo-able loop. After all that, we set the component height and width to the timer property. Lastly, we call the next animation frame and run our update method again. We end up with a green visual countdown component that shrinks every frame until it gets to nothing and then resets to 200 pixels wide again.

In addition to setInterval and requestAnimationFrame, other frameworks and libraries we may want to use might have their own ways to call a timed update method like this. For example, if you use a 3D library like Three.js or Babylon, they both have their own render hooks you can tap into, so you’d implement your component a bit differently.

The point is that the Web Component lifecycle doesn’t come with an update method like many other component lifecycles you might see. Because web technology can be used for so many different things, it’s not wise to dictate how you should do it. Most of the time in my own work, I never need that update method. Even simple UI animation can be handled through CSS. And of course, when I do, I like having the choice of which method to use.

Maybe you, in your own personal use cases, always need some sort of update method like Unity has. It certainly makes sense if you are a game developer or similar and need a render/update method to drive your game and animation.

If this is the case, you’re still covered. Web Components support inheritance, and we can go one level deeper and just add on to the existing component lifecycle. Let’s steal code from our visual countdown timer animation example and use our requestAnimationFrame call to power it.

Listing 4.10. Creating an inheritable base for components to update every frame
<html>
<head>
  <script>
       class GameComponentBase
               extends HTMLElement {               1
           constructor() {
               super();
               this.onUpdate();
           }

           update() {}                             2

           onUpdate() {                            3
               this.update();
               requestAnimationFrame( () => this.onUpdate());
           }
       }

        class VisualCountdownTimer
               extends GameComponentBase {         4
            connectedCallback() {
                this.timer = 200;
                this.style.backgroundColor = 'green';
                this.style.display = 'inline-block';
                this.style.height = '50px';
            }

            update() {
                this.timer --;
                if (this.timer <= 0) {
                    this.timer = 200;
                }
                this.style.width = this.timer + 'px';
            }
        }

        customElements.define('countdown-timer', VisualCountdownTimer);
    </script>
</head>

<body>
    <countdown-timer></countdown-timer>
</body>
</html>

  • 1 Class provides a base for building game-style components
  • 2 Update method to be filled out by component using the base class
  • 3 Internal update method to keep requestAnimation frame going
  • 4 Actual component class, which extends the base component

So, in the example in listing 4.10, we’re still doing the exact same simple animation: making a countdown indicator graphic shrink. But we’ve pulled the logic that deals with creating an update event every frame out into its own class. Note that I say class and not component because we’ve done everything to create a new component except define a custom element and map that to a tag.

Instead, we’re creating a base class, GameComponentBase, that components can inherit from. Figure 4.4 shows this chain of inheritance, all originating from HTMLElement. I did something a bit tricky, though. Instead of directly calling the update method, I have a different method—onUpdate:

 onUpdate() {
    this.update();
     requestAnimationFrame( () => this.onUpdate());
 }
Figure 4.4. Using inheritance to create a subclass of HTMLElement to enable frame updates like a game engine

The reason is best explained by doing it a way I would not suggest first. Let’s not use both, and only use update.

Listing 4.11. Simpler example with just one overridable update
class GameComponentBase extends HTMLElement {
    constructor() {
        super();
            this.update();
        }

        update() {
            requestAnimationFrame( () =>
               this.update());              1
        }
    }

  • 1 Single update method

This new GameComponentBase is still good and can be used in pretty much the same way, but let’s take a look at how we’d use it.

Listing 4.12. Using the simpler base class
class VisualCountdownTimer extends GameComponentBase {
    connectedCallback() {
        this.timer = 200;
        this.style.backgroundColor = 'green';
        this.style.display = 'inline-block';
        this.style.height = '50px';
    }

    update() {
        this.timer --;
        if (this.timer <= 0) {
            this.timer = 200;
        }
        this.style.width = this.timer + 'px';
        super.update();                        1
    }
}

  • 1 Now required to call super.update()

Notice we’ve simplified our GameComponentBase class a bit. We’ve condensed the two update methods into one, but in our VisualCountdownTimer component, we’re now forcing anyone using GameComponentBase to call super.update(); every time! Of course, with inheritance, we don’t call update on our underlying GameComponentBase unless we use super.update(). I don’t know about you, but I’d make a new component and forget to call super.update() most of the time. A little planning like this up front can make the developer experience happier.

Unity has two more lifecycle methods, OnDisable and OnDestroy, which serve the same purpose as Web Components’ disconnectedCallback: to clean up after disabling or destroying the component.

4.6. Component lifecycle v0

The Web Components API seems pretty solid now, doesn’t it? We’ve compared and contrasted it to other component lifecycles, and I hope you have a pretty good feel that it’ll work well for anything you throw at it. I don’t expect that you’ll know each and every method by memory, especially when starting out. We all have to Google syntax occasionally. One caveat when you do look up usage with Web Components is that it’s a relatively new standard, and it’s already gone through one revision.

What this means is that when you look up syntax, you might accidentally stumble on the old methods. Currently, we are using v1 of the Web Components API. What came before was dubbed v0, and v0 won’t work anywhere except for where it was originally implemented: Chrome. Even there, as time moves on, it will be more and more spotty.

Important

The Web Components API has changed!

Not much has changed really (see table 4.1), though the first thing to note is that instead of letting you use the constructor in v1, you use the createdCallback method.

Table 4.1. Custom Element/Web Components API changes

Method calls

How it changed

Deprecated: createdCallback Current: constructor In v1, the more standard constructor replaces the createdCallback.
Deprecated: AttachedCallback Current: connectedCallback In v1, to listen for when your element has been added to the DOM, you use the connectedCallback; in v0, it was the attachedCallback.
Deprecated: detachedCallback Current: disconnectedCallback The old way of listening for when the element is removed from the DOM, now the disconnectedCallback in v1, was the detachedCallback in v0.
Former: AttributechangedCallback Current: attributeChangedCallback and observedAttributes The last change is the attributeChangedCallback in v1. The name actually hasn’t changed here, but the usage has. Now you need to make sure to define those observedAttributes, as we discussed in the last chapter, to tell the component what attributes you’d like to listen for. Previously, this callback would just listen to everything.
Deprecated: docment.registerElement Current: customElements.define Lastly, outside of the component lifecycle API, the way you register your element has changed as well. Currently, we use customElements.define('my-web-component', MyWebComponent); Formerly, in v0, we would use document.registerElement('my-web-component', MyWebComponent);

Summary

In this chapter, you learned

  • How to round out the lifecycle methods you’ve already learned with the remaining two methods: disconnectedCallback and adoptedCallback
  • The concept of garbage collection, and why you would clean up after your component
  • How to subclass a Web Component and use it as a base to provide common functionality, like frame-by-frame animation, to other components
  • Differences for and similarities to the React and game engine lifecycle methods, and how even though both have more methods to their APIs, Web Components don’t fall short
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.111.9