One of the joys of coming to Web Components from a Vue.js perspective is that, well… they are pretty similar. Actually, according to the Vue docs:
You may have noticed that Vue components are very similar to Custom Elements, which are part of the Web Components Spec. That’s because Vue’s component syntax is loosely modeled after the spec.
So, what do Custom Elements do?
They provide a way to create fully-featured DOM elements, that are both conforming and functional. Until now nothing stopped a developer from adding custom tags in her HTML code and call it a day, but these were non conforming and not very functional.
Custom Elements help us add new components that inherit from the HTMLElement interface, including all its properties and methods.
On top of that, we can create Custom Elements than can extend from even more specific elements, like HTMLButtonElement, and thus inherit all the properties of a <button>.
How do we create a Custom Element?
This is where our expertise in Vue.js kicks in. This is how we create a Custom Element:
That is awfully similar to the Vue way:
After creating a Custom Element, we just need to use it:
Wait, wait, there’s more. Custom Elements offers an API to interact with them on different moments of their lifecycle. There are specifically 5 “hooks” you can use:
- constructor (): Triggers when the element is upgraded (when an element is created, or when a previously created element becomes defined).
- connectedCallback (): Triggers when the element becomes connected (that is, when it’s inserted into a document, including a Shadow Tree). Given that we can’t insert properties on the constructor() (we’ll talk about this later), this is a perfect time to bind our data.
- disconnectedCallback (): Triggers when the element becomes disconnected (removed from the document).
- adoptedCallback (): Triggers when the element becomes adopted into a new document (honestly, I’m still wrapping my head around this: when do we move an element from document to document…?).
- attributeChangedCallback (): This is the real gold here. This function triggers when any of its attributes are changed, appended, removed, or replaced. Basically it allows us to take an action every time an attribute of the element gets modified. It accepts three parameters: name, oldValue, newValue.
Now that we know which methods are available to have some control over the lifecycle of Custom Elements, how do they map to the Vue.js components ones? Some of them are straightforward, some of them are not:
constructor = created
This seems to be an easy one, but it’s actually not that simple. Turns out that this methods has some limitations, the most relevant being: “The element must not gain any attributes or children”, and “In general, work should be deferred to connectedCallback as much as possible”.
This means that constructor and created are not to be used for the same purpose. We can still execute functions, but not define data or attributes.
connectedCallback = mounted
I found this to be a quite good match, as in Vue.js mount gets invoked immediately after a component has been inserted to the document, and according to the spec, connectedCallback gets called when the insertion steps have been invoked.
attributeChangedCallback = beforeUpdate
In Vue.js, update gets called after the DOM has re-rendered, so the closest hook to attributeChangedCallback would be beforeUpdate, called after the data in the component has changed and before the template has been re-rendered.
I’m hoping that in the future, as soon as we introduce Shadow Tree to the mix, we will be able to add a hook for Custom Elements that is similar to update in Vue.js.
disconnectedCallback = destroyed
When an element gets removed from the document, disconnectedCallback fires. That is pretty similar to destroyed, which gets called after the actual component has been removed from the rendered tree.
Customised Built-in Element
The Custom Elements we’ve reviewed so far are the so called Autonomous Custom Elements, which are the closest equivalent to Vue Components. They are built from scratch and only related to the abstract HTMLElement interface.
There’s another type of Custom Element, called Customised Built-in Elements. These are similar to the autonomous ones, but instead of inheriting from HTMLElement, they do from any already existing component in the HTML specification.
This saves us some time and some headaches in case we want to build a custom button, or input field. You can find a detailed explanation here.
This is interesting and Vue.js (as expected, it’s not its job) doesn’t allow us to do so. We can extend other components, and add mixins, and so on, but not built-in HTML elements.
Data, and watched, and methods!
Here’s the deal: changing attributes on a Custom Element object by doing a regular assignment won’t trigger attributeChangedCallback. This means that we need to actually call setAttribute on the object.
This is cumbersome, and the way to solve this is quite similar to what Vue.js does: use the Object.defineProperty. There’s a lot of literature about this method. Like a lot. The way we could use it on our implementation of Vue for Custom Elements would be something like this:
Easy peasy. We can even implement watch from Vue.js easily by using attributeChangedCallback (this functions is simplified, but you get the idea):
One thing to mention is that a Custom Element will only trigger attributeChangedCallback if it’s included in an array of observedAttributes in the element declaration. We could do something like:
You get the general idea, uh? Although we lose a lot of the intricacies and possibilities of Vue.js, it’s possible to implement a barebones library that ports the main ideas of the library to Web Components.
Trying to learn what the code in Vue.js does can be intimidating. Trying to replicate what it actually does (in broad strokes) by coding a small similar library helps A LOT.
In the next chapter we’ll try to emulate the rendering using the Shadow DOM. Stay tuned!