Introduction: Why Build a Web Component with Vanilla JS?
Greetings friends, welcome to a sort of code. Today we're going to create a web component using plain old JavaScript, so no libraries and no frameworks. This should give us an appreciation of what's going on under the hood in a web component and also what those libraries and frameworks actually provide for us, which is quite a lot. You know, the web component spec is a quite a low-level spec, so you'll see there's quite a lot of boilerplate and ceremony involved in creating this just using vanilla JavaScript.
Before we jump in, if you find yourself enjoying this video, feel free to click that subscribe below and the alert Bell icon to get notifications of upcoming videos. Let's jump in though. I'm going to create a very simple counter, a very simple counter component, just using JS. Let's see how we get on.
Creating the Component Class and Shadow DOM
Okay, so I'm going to create a counter.js file here, and I'll make it a bit bigger. So we'll have a class MyCounter and we need to extend HTMLElement. So that gives us quite a bit of web component functionality and a lifecycle for free.
Let's create a constructor and call the base class. Now what we need to do here is create our shadow DOM. So this is the encapsulated part of our web component. It's the bit where styles from outside the component can't leak in and styles can't leak out as well. So any of your CSS, you're not going to get any naming conflicts. Right, what we call is attachShadow and we just pass in an object with some options of mode, just mode, and the reason for this one is you can have mode as 'closed' or 'open', and it just means whether you can access this shadow DOM via JavaScript. In your components, you're always going to want this open. Yeah, we'll see we're going to be using that one we try and attach the events to this component as well.
Rendering UI and Using connectedCallback
Okay, so we've got a shadow DOM. We can add some UI into this shadow DOM now. So in our render function, I can grab this shadow and I can grab its innerHTML. Now, here's something straight away that a library or framework is going to abstract away from you. It's going to allow you to add your UI in a much more efficient, maintainable, usable manner. Using innerHTML is going to cause us problems, even in this little simple example. It'll cause us problems with the event handlers. In a more complex scenario, you're going to have problems with it being slow. You're going to have problems with updates. It's just, yeah, you don't want to be using innerHTML. Unless we're going to now essentially write a framework ourselves or a library, then this is what we're going to use. So we'll see the problems with it.
Let's put a title in here of 'Counter' like so. And we'll display our count. So we haven't got a property count at the moment that will just come out as undefined when we view this, but that's fine for the moment. We'll create a button and give it an ID so I can reference it later, and we'll call that 'increment' like so. That should give us a very simple UI there. We've got nothing calling this render at the moment, so we wouldn't get it displayed. So here comes the first lifecycle event in a web component. We have connectedCallback. So because we're inheriting from HTMLElement, just going to make it slightly smaller so we see this, we can override connectedCallback, and when our element is added to the page, we'll get this called. So we can simply call render at that point and we'll get something up on the screen.
Defining and Using the Custom Element
Alright, now to register this custom element, we need to call customElements.define. And we need to pass in a custom element name, so we're going to use my-counter. Now note, we need a hyphen in there. That's so that this custom element will not conflict with any existing HTML tags like your button, your div, your body, your html, anything like that. None of those have a hyphen in. That's why we use a hyphen. That means we won't conflict now, and we won't conflict with any new HTML tags that come in a future HTML release.
Okay, and we pass in our class as well. So that sets us up, and that means we can use this tag now in a HTML page as long as we include this script. So let's give that a go.
I'll create an index.html and we shall add in here my-counter and we'll add the script that gives us its functionality which is counter.js. So I should be able now, if I open this up with a live server, to be able to see that.
Passing State: Attributes vs. Properties
And there we go. I'll get rid of that on there. So we have got, if I go back here, we've got our counter, which is our header here. We've got undefined, which is fair, for our count property, we haven't defined that yet. And a button that does nothing at the moment.
Let's look then at setting up this property. What we would like, because this will show us how to sync properties and attributes. And on that note, an attribute is when we set something up on the tag here in the page. So if I do count="10" here, this is an attribute and it has a corresponding property within our JavaScript. So attributes on the tag, properties in the JavaScript. So we want to be able to set this count up here. What do we need to do? Nothing. Nothing separate at the moment. count is still undefined. We need to say in here which properties, which attributes we're going to be using. And we want to use the count one.
Observing Attribute Changes with Callbacks
How to do this? We set a static get method called observedAttributes. This is static because it will be called across all instances. All instances of my-counter will be interested in the count property. Things like so. It's not on an instance-by-instance basis; it's global, that's why. So that's why it's a static one. And we just need to return in here an array of them. And we're only interested in one; we're interested in count. You know, you could have various other properties on here if you wanted, but for now we'll just have the count. So this tells the system, yeah, we want to observe the count.
But we also then need to provide another callback so that when count changes, we can do what we want to do. And we do that, similar to connectedCallback, we're going to override another lifecycle method, and this one is called attributeChangedCallback. And this one takes three properties. It takes the name of the attribute that changed, we'll call it prop, the oldValue, and the newValue. So these can be named anything you want, you can name them value as long as they're in that order, that's fine. So in here, we're going to say, alright, has the attribute or property you know, property, actually the same thing, but yeah different, if the property is equal to count then let's update our component.
Syncing State with Getters and Setters
Now the reason we're not storing away the new value here is what I'm going to show you now. We want to keep the attributes and the properties in sync. Now, the easiest way to do this is have one golden source. So rather than creating a property on here and giving that a value, we're going to create a property on here using getters and setters, and the value it returns will be the attribute value. So let me show you how that works. Let's make it a little smaller again.
So if we want to get the count, so we've got a getter here for the count, what we're actually going to do is return this.getAttribute and get the value of that attribute. So it's going to come in here and grab that value. So that's our golden copy. This way we're not going to get any problems when we have set count as well because in here we're actually going to do this.setAttribute and we're going to set the count attribute to a new value which gets passed in here like so. If we've got two properties, we can get into a situation where we set it and we get into an internal loop with attributeChanged. This way, we're only ever setting or getting the attribute value, and then we're using that internal account.
So you can see over here now we're getting the value 10 coming up because this is all set up. We've set and we said, yep, we want to know about count. When it changes, when count changes we call render. render accesses the count property which will come to this get and call getAttribute('count'). So I can come in here now and give that a value of 20, and that will update. Now, that did a full page refresh. So that route is it comes into here and we called render. render calls this count and that calls get and getAttribute. But that won't work, well this will work at the moment, but it didn't use these. These become useful when we update this property via code on the page.
Programmatically Updating Component State
Let me show, let's let's see, let's see that in action. So if I grab my button... well, grab the counter actually. So let's give this an ID of counter. And if we say... if counter is... So I'll get hold of the counter like so, and then we do a setAttribute for count and we set it to a hundred. And let's put this inside a function, update, like so. And we'll add a quick button on here and hook that up to update. So what do we got now? We've got a bit more space there. We've got a button that is going to call this function, which will grab a reference of our button and change the attribute to a hundred. Okay, so I'm going to save that off and call update, and it works. It works that because we're getting a call into this attributeChangedCallback, and we've said we want to observe changes. If we weren't looking at count, if we were looking at counter, and I come back in here, give this a refresh and do update, it's no longer updating it. Yep, so yeah, it worked when we did a page refresh, but to get it to work programmatically, you need to have attributeChangedCallback and observedAttributes all set up correctly. So I'll put that back to count. Click update, go to a hundred. Okay.
Handling Internal Events and Re-Rendering
Let's get rid of that. We don't need that anymore. Don't need that. So back to our normal counter with the incrementing not working. Let's have a look now. So that was all about syncing from changes here to this attributes. What about changes made to the property in our JavaScript? We need to hook this up now. Because we're using innerHTML, it's a little bit more complicated because I can't add the event handler here. So I'm going to add the event handler after we've created it here.
So I'm going to get hold of the button. I can use the ID here to get hold of the button. So let's do button =... Now to access the DOM we need to use our shadow reference again, this.shadow, and then we can just do a normal querySelector and grab the button. And then I want to grab... I want to add an event listener to this button. And I want it to be for the click event. And when that happens, I want to call our method to increment it. So let's just create an inc here. So this.count++, and we're going to call this.inc. We get into the world of this and bind here. I'm not going to try and go into this too much here, but basically you're going to need to do a bind(this) in order for this to work. That was the word, this a lot, it's all... you can look up a video on that at some point if you want to get into binding in this, but by putting that there, it means this will get called for us.
Okay, that looks pretty good. The problem we'll have is whenever we call render, we will place a whole new innerHTML in, and that will ruin our listener. So I'm going to copy this and also put it in here. So there's way better ways to do this, but you know, you've got to, you've got to know how to do them and you're essentially doing the library, you know, what you do is start to write the code that's in a library that does this for you. Okay, so let's save that off and give it a click. And that is going up correctly. Good.
Final Review and Why Frameworks Help
If I look in the tools here, just make it a bit bigger, we can see our count is going up to 25, 26. So it's updating the attribute, and that is our golden copy of that value, that sync between the attributes and the properties. So that's a fair little bit of code. And that's what, one property, one method.
Yeah, if you're gonna make a complex component, you don't really want to be getting into this. You want to include LitElement or you Svelte or Stencil or or Vue or React or, you know, whatever. You don't want to be doing, you don't want to be doing this. It's going to, it's going to be hard to maintain. It's just going to be inefficient. It's going to be slow. You're going to get problems. But it's good to have an understanding of the flow, the lifecycle events, this connectedCallback, the attributeChangedCallback, the observedAttributes, and setting up that your shadow DOM and and and how that works, and then, you know, the problems with it there. So there you go, a web component. Hope that was good.
If you liked this video, give me a thumbs up. If not, a thumbs down, but feel free to subscribe for more upcoming videos. Thanks very much for watching. See you next time. Bye.
Outro
you you