DEV Community

Cover image for Web Components API: Definition, Attributes And Props
Mihaela for WorksHub

Posted on • Originally published at javascript.works-hub.com

Web Components API: Definition, Attributes And Props

After learning what Web Components are, their history and benefits, it's time to dive deeper into their API component and start doing some coding.

In the following sections, we will go through the key concepts of the Web Component API, which allow us to create a Web Component:

  • Definition, attributes and props
  • Template, Slots, ShadowDOM vs LightDOM
  • Lifecycle events and custom events
  • How to style Web Components

Today we'll try to define web components and talk about two of their most important and sometimes hard to understand characteristics - attributes and properties.

Before we get started, we need to take a quick detour to a concept mentioned in the last article: custom elements.

First things first: how can a custom element be defined?

Custom elements: definition and creation

The web components creation is based on the standard ES6 Class API. As seen before, custom elements allow the definition of custom HTML tags (and therefore, DOM elements) and their attributes. Despite being custom, they are standard as well, that's why they extend for HTMLElement:

class MyCustomElement extends HTMLElement {
  // our custom element code
}
Enter fullscreen mode Exit fullscreen mode

The line above is both simple and powerful. It allows us to create native HTML elements as if they were part of the official standard.

That is not all, not only are we able to create our custom elements but we can also extend the existing ones. To do that, instead of extending from the generic class HTMLElement, we have to do it from the class we want to extend from. For instance, if we want to create a custom element that extends the functionality of the native anchor element, we could do it by defining our custom element as follows:

class MyCustomElement extends HTMLAnchorElement {
  // our custom element code
}
Enter fullscreen mode Exit fullscreen mode

This is what we call customized built-in elements. We'll see more of it later when we talk about web components' composition.

As our custom element class is extending from a different one, in order to follow the inheritance hierarchy, it is required to establish the correct prototype chain and set this value before any further code is run, to make a parameter-less call to super() as the first statement in the constructor body.

class MyCustomElement extends HTMLElement {
  constructor() {
    super();
  }
  // our custom element code
}
Enter fullscreen mode Exit fullscreen mode

Once we have our custom element class ready with all its features and behaviour already coded, we need to define the custom element. So the full example would look like this:

class MyCustomElement extends HTMLElement {
  constructor() {
    super();
  }
  // our custom element code
}

customElements.define('my-custom-element', MyCustomElement);
Enter fullscreen mode Exit fullscreen mode

If you're a one-liner lover, the one-line version is possible by using an anonymous class:

customElements.define('my-custom-element', class extends HTMLElement {
    constructor() {
      super();
    }
    // our custom element code
  }
);
Enter fullscreen mode Exit fullscreen mode

Apart from the define method, there are other methods available in the customElement object:

  • get(name): If a custom element with the given name it has been defined, it will return its constructor, otherwiseundefined
  • whenDefined(name): Returns a promise that will resolve when the custom element with the given name is defined. It will reject if the given name is not valid or no custom element exists. This method can be used to avoid executing an action until the target custom elements are defined.
  • upgrade(): Allows upgrading elements. Elements are automatically upgraded when they become connected, but this method can be used if you need to upgrade before you connect the element. It's not very common to use it but it's available when needed.

Notice that we are using the customElements object directly without defining it. That's possible because this object is available on the global Browser Window Object window. This object is always available in the browser environment in the global scope. When the JavaScript engine doesn't find a variable in the current scope it finds them in the global one, and specifically in the window object. That's why, in our case, typing customElements.define(...) and window.customElements.define(...)is the same.

It's also worth mentioning two other important features that we might cover in a future article - forms and accessibility features.

To make a custom element a form-associated custom element, just add the following static method (returning true)

static get formAssociated() { return true; }
Enter fullscreen mode Exit fullscreen mode

On the other hand, the ElementInternal interface has functions and properties useful both for form elements as well as for accessibility features. To get an instance of it, call the attachInternals() method:

this._internals = this.attachInternals();
Enter fullscreen mode Exit fullscreen mode

You can get more info about ElementInternal on the WHATWG site (Web Hypertext Application Technology Working Group).

Finally, it's important to highlight some key points that need to be taken into account when creating custom elements:

  • The name of the customs element must contain a hyphen/dash (-), therefore, it has to be formed of at least two words. This requirement was defined in order to make the HTML parser able to distinguish custom elements from native ones. As you probably have already realized, native elements tags are "one-word" tags.
  • The name of the custom element must be in lowercase, i.e. it has to follow the kebab-case convention.
  • The name of the tag and its class, doesn't have to be the same, but it's recommended for them to coincide. For clarity, some authors, like to add the Component sufix to the class name.
  • Tags can not be registered more than once, so, obviously, different custom elements cannot share the same tag definition.
  • Custom elements cannot be self-closing by definition. Only void elements can be self-closing. You can read more about self-closing elements and their complete list in the official HTML SPEC

To be clear about naming custom elements, let's take a look at some invalid/valid examples:

  • <mycomponent> --> <my-component>
  • <MyComponent> --> <my-component>
  • <My-Component> --> <my-component>
  • <MyCustomComponent> --> <my-customcomponent> or <my-custom-component>

Attributes

Attributes are a HTML related concept. Attributes are the way we define the characteristics of an HTML element, i.e., an HTML tag. The same applies to Web Components.

In the example below, src, alt, widthand height are attributes.

<img src="/img/profile/MartyMcfly.png" alt="A profile picture of Marty McFly" width="200" height="200" />
Enter fullscreen mode Exit fullscreen mode

When the browser engine parses the HTML code to create DOM objects, it recognizes standard attributes and creates DOM properties from them. It's worth to highlight that this only occurs with standard attributes, not with custom ones. Please note that standard attributes are not the same for all elements. For instance, idis a standard attribute common for all the elements, while alt attribute is exclusive of <img />.

For the custom attributes that are not automatically reflected properties, as well as for operating with attributes in general, we have the following set of methods available:

  • element.hasAttributes(): Checks whether the element has attributes or not.
  • element.hasAttribute(name): Checks whether the element has the attribute with provided name.
  • element.setAttribute(name, value): Checks the attribute called name with the given value.
  • element.getAttribute(name): Return the value of the attribute called name or null if it doesn't exist.
  • element.getAttributeNames(): Returns an array of the element's attributes.
  • element.toggleAttribute(name): Toggles boolean attributes the following way: removes them if they exist, creates them if not.

One thing to be careful about is defining your own attributes for the web component. They don't have to collide with any native attributes and shouldn't confuse the developer about their nature (native or custom). On the other hand, it's important to highlight that attributes should be used only with primitives types as string, number and boolean.

Listening to attributes changes in web components

When developing a web component, we'll probably need to execute actions when some attribute change. For instance, if we have a value attribute and this is set, we'll want to render its (new) value. The custom elements API brings us a way to observe attributes changes. It consists of two related methods: observedAttributes and attributeChangedCallback.

The first one is where we define which attributes we want to observe. To do that we have to return an array with the names of the attributes. In the following example, we want to observe changes in attributes called value, min and max:

static get observedAttributes() { 
  return ['value', 'min', 'max']; 
}
Enter fullscreen mode Exit fullscreen mode

The other method allows us to execute actions when an attribute has changed. It takes into account three parameters:

  1. The name of the attribute that changed and therefore has fired the callback,
  2. The old value of the attribute that changed,
  3. The new value of the attribute that changed.

Find below an example of a attribute called "value" that has changed from 7 to 11:

attributeChangedCallback(name, oldValue, newValue) {
  // name will be "value"
  // oldValue will be "7" 
  // newValue will be "11" 

  // your code...
}
Enter fullscreen mode Exit fullscreen mode

Properties

Properties are a JavaScript-related concept. They are part of the DOM Node interface, an abstract base class upon which many other DOM API objects are based (more info about it in (MDN Web Docs). They are used to access to DOM Node interface features, like Node.parentNode, Node.childNodes, Node.className and many more. We can also define our custom properties, to define the custom behaviours of our custom elements.

Properties can have any value and they are case sensitive.

Reflecting properties to attributes (and vice-versa)

In some way, we have already described the relation between properties and attributes. Properties are the Javascript representation of HTML tags attributes. Meaning that when an attribute is modified, we expect that its property value will be also modified and vice-versa.

You can check this behaviour with the following snippet, where you will see that the src attribute will change after modifying the property value (after 1'5 seconds to be able to see the original value).

  <img id="myImg" src="yourPicture.jpg"/>
Enter fullscreen mode Exit fullscreen mode
setTimeout(() => {
  document.querySelector('#myImg').src="otherPicture.jpg";
},1500)
Enter fullscreen mode Exit fullscreen mode

As previously mentioned, the browser engine creates properties for those standard attributes that it recognizes, but not for the custom ones. For the custom ones, we need to do it explicitly when defining our custom element.

We could do that by defining getter and setter methods for the properties we would like to reflect its value to its namesake attributes. To do the magic, we need to set and get the attribute value under the hood.

Let's suppose that we have a custom element whose class has a property called color that we want to reflect its namesake property. Give this scenario, the code would be the following :

//'this' is pointing to the custom-element declared class 
// that extends from HTMLElement, that's why has the 
// 'setAttribute' and 'getAttribute' methods available

set color(value){
  this.setAttribute('color', value)
}

get color(){
  this.getAttribute('color')
}
Enter fullscreen mode Exit fullscreen mode

Behind the scenes, what is happening is that when we execute this.color = 'blue' what is really being executed is a call to color's set method with a param value of 'blue', which will set the attribute's value to 'blue'. On the other hand, when we execute this.color what is really being executed is a call to color's get method, which return the attribute's value.

If you don't remember well how property getters and setters work, you can have a look at MDN Web Docs

All these attributes and properties are a bit hard to understand by just looking at the code, it will get easier once you'll see it working in a real custom element, something we'll do in the next chapters. Meanwhile, remember the trick to achieve reflection: linking get/set property methods to get/set attribute methods

Maybe this schema will help clarify how attributes and properties reflection works:

Untitled Diagram-2.png

Conclusion

We had a great tour of the minimum we need to build a web component. So far we know how to define a custom element and its attributes/properties. In the next chapter, we'll talk about templates, slots and ShadowDOM vs LightDOM.

Make sure to read the first article of this series as well:

Don't forget to sign up to WorkHub for more articles like these!

Originally written by Rafa Romero Dios for JavaScript Works

Top comments (0)