In my last article, Custom Web Components, Part 1,
I discussed how one did not necessarily need JavaScript code to make use of custom parts of the Web Components system for better wrangling of HTML and CSS.
But that's obviously not the point of Web Components. Web Components are meant to encapsulate and share more advanced functionality than fancy DIV tags with drop shadows.
So, what does a basic Web Component look like? The absolute minimum amount of code necessary to define a custom tag is:
class MyElement extends HTMLElement {
}
customElements.define("my-element", MyElement);
Okay, okay. That wasn't fair. What is the bare minimum amount to be able to functionally do anything?
class MyElement extends HTMLElement {
constructor() {
super();
}
static observedAttributes = [
// e.g. "disabled", or "mywholenewattributename"
];
attributeChangedCallback(name: string, oldValue, string, newValue: string) {
}
connectedCallback() {
}
disconnectedCallback() {
}
}
customElements.define("my-element", MyElement);
- Only ever extend the basic HTMLElement, or other element types that you create. Don't extend any of the built-in elements. It's supported in Chrome and Firefox (mostly), but it's not supported in Safari and won't ever be. Apple is demanding a rethink to how customized built-in elements work. Which sucks for us trying to get things done. But it's also good, because the current implementation does indeed suck.
- If you want to create a customization of, say, HTMLSelectElement, it's still possible by using composition instead of inheritance. Don't forget that, in TypeScript, classes can implement anything as an interface, even other classes. So, you could have MyElement implement HTMLSelectElement, then use an interior <select> tag to forward calls for the interface implementation.
- The constructor must be a parameterless constructor. When you go to create an instance of your element, you will call document.createElement("my-element"); which does not provide any facility to provide parameters to the constructor. It's okay if intermediate classes in the middle of an inheritance hierarchy take parameters that get passed by the child classes, but the ultimate leaf of that tree needs to have a parameterless constructor.
- You cannot add child elements to the element or change attribute values in the constructor. You can set event handlers. You can also create and set fields or properties in the element, just as long as they don't involve calling one of the (set/get/toggle)Attribute methods. You must wait until the constructor has finished before the children or attributes of the element can be changed.
- Only the attributes listed in the static observedAttributes array will be reported by attributeChangedCallback.
- I'll often stick "if(oldValue === newValue) return;" at the top of all of my attributeChangedCallback's to bomb-out early, because attributeChangedCallback will fire for every call of setAttribute, regardless of whether it had actually changed.
- The connectedCallback and disconnectedCallback are called when the element is first inserted into/removed from a document tree. It's not whenever the element is added to another element. That other element could itself not yet be in the document tree. It's only when the element is added to another element that is already in the document.
- Most of the time, I have some internal changes to the element that I want to make, anything that involves modifying the children or attributes of the element. Often, connectedCallback is the best time to do that. It's not the only time, or the earliest time, but it's certainly the last time you can change the structure of the element before anyone will see it. It's easy to listen for and you can reliably capture it.
- But sometimes, connectedCallback is not the best, because it can actually be called multiple times: once for every time the element is added into the document, if it had gotten removed at any point. Sometimes, I find it best to undo in disconnectedCallback whatever changes I did in connectedCallback. Sometimes, I find it best to have some kind of logic to make sure the code I'm running in connectedCallback happens only once (e.g. a this.isFirstTime flag that is set to true in the constructor, then false at the end of connectedCallback).
These are the basics. You could get started from just this. You should get started from just this. You should play around with these features and then, when I have the next article ready, you'll have lived experience with some difficult warts that I will cover how to handle in the next article.