DEV Community

Cover image for Web Fundamentals: Web Components Part 2

Web Fundamentals: Web Components Part 2

Hasan Ali on February 08, 2024

Contents Recap More with Custom Elements React, not overreact Summary 1. Recap So far, we’ve defined what web components a...
Collapse
 
dannyengelman profile image
Danny Engelman

What is the point of attrs ??

 static attrs = {
    interval: "x-interval",
  };
  static get observedAttributes = [Timer.attrs.interval];  
Enter fullscreen mode Exit fullscreen mode

over

  static get observedAttributes = ["x-interval"];  
Enter fullscreen mode Exit fullscreen mode

When this.constructor.observedAttributes always gets you the attributes Array (if required at all)

Collapse
 
hasanhaja profile image
Hasan Ali

It just acts as a map, and it's not required at all.

I saw that pattern when looking through other web components in the wild, and thought it was a nice way to organize attributes in one place, and maybe even alias them like we've done there.

Collapse
 
dannyengelman profile image
Danny Engelman

So the next question is: What is the point of aliasing?

Thread Thread
 
hasanhaja profile image
Hasan Ali

Convenience, really. If you wanted to decouple the API from the implementation, this makes it a little more convenient to change things in the future. For example, if I wanted to rename the attribute, I can do it in the attrs object and not have to refactor the rest of the component where I've addressed it.

Do you think there's a better way to do this? The simple alternative I can think of is use the attribute string directly, and then do a find-and-replace if things change.

Thread Thread
 
dannyengelman profile image
Danny Engelman • Edited

Good point "future changes"

You will have a cleaner and smaller class when you declare all that sh* outside the class (at the top of your file)

const interval = "x-interval";
const my_component_attrs = [ interval ];

customElements.define( "my-component" , class extends HTMLElement {
  static get observedAttributes(){
   return my_component_attrs;
  }
...
Enter fullscreen mode Exit fullscreen mode

Note #1 All good minifiers will replace references with the const value, so you get smaller code as well

Note #2 my-component is often parameterized (is that a word?) as static tagName on the class in recent examples as well.
WHY?!? It is available on the component instance as this.localName or this.nodeName

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

You might have noticed that the timer declaration has moved from the constructor to the connectedCallback, and this was to simplify passing in the dynamic interval delay value, which would only be queryable once the element is mounted (or connected)

There's two things I want to add here:

  1. Setting up the timer in the constructor and removing it in the connectedCallback would have meant the component would have stopped working if it was ever removed from the document and inserted elsewhere. Setting it in the connectedCallback to re-start it when re-attaching is the correct way about this.

  2. It is not entirely correct that the attribute for the delay value is only queryable when the element is mounted. If the element has already been parsed by the time the custom element is defined, the constructor will have full access to the element and its children, including attributes. Only if the custom element is already defined before the element is parsed, the constructor will run before any of the attributes or child elements have been parsed.

Note: This is equivalent static observedAttributes = ["x-interval"];, and if you had more than one attribute to track, you would comma-separate them in that array.

If you already have an attrs attribuet, then I would hope nobody would actually comma-separate them and instead just write something like this:

static observedAttributes = Object.keys(Timer.attrs)
Enter fullscreen mode Exit fullscreen mode

Last but not least, the final version of the component would behave somewhat inconsistently. Detaching the component would initially pause the counting. Updating the interval attribute would then resume the counting. Connecting the element again would then start a second interval that could never be cancelled anymore and would continue to count up until the page is closed.

An easy way to fix this would be to just check if the object is actually connected in the attributeChangedCallback 😁

Collapse
 
hasanhaja profile image
Hasan Ali

Thank you very much for your feedback.

Setting it in the connectedCallback to re-start it when re-attaching is the correct way about this.

This makes so much sense, and I don't know how I missed this!

It is not entirely correct that the attribute for the delay value is only queryable when the element is mounted. If the element has already been parsed by the time the custom element is defined, the constructor will have full access to the element and its children, including attributes. Only if the custom element is already defined before the element is parsed, the constructor will run before any of the attributes or child elements have been parsed.

This has taken me a quite a while to wrap my head around. I had someone else also flag this for me when I did the first post, and to be honest it hasn't clicked until now. I'll work on the correction and update the post. Thank you!

static observedAttributes = Object.keys(Timer.attrs)

Good shout! I'll add that in too and signpost that by doing so all of your attributes will be tracked, which is probably what you'd want most of the time.

Detaching the component would initially pause the counting. Updating the interval attribute would then resume the counting. Connecting the element again would then start a second interval that could never be cancelled anymore and would continue to count up until the page is closed.

Great catch! I definitely didn't play that scenario out. I'll make the correction!

Honestly, thank you so much for taking the time to read the post and give me feedback! Greatly appreciate it.

Collapse
 
alexroor4 profile image
Alex Roor

Your article is a fantastic resource for anyone looking to delve deeper into this subject. I'll definitely be sharing it with my colleagues.

Collapse
 
hasanhaja profile image
Hasan Ali

Thank you for your kind words!

Collapse
 
alexroor4 profile image
Alex Roor

This is definitely a great article! I was delighted with the first part, and then there’s the second! thank you

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

One pattern I end up repeating for pretty much every slightly larger component I write is this:

attributeChangedCallback(attribute, from, to) {
   // snake-case to camelCase
   const name = attribute.replaceAll(/-[a-z]/g, str => str.slice(1).toUpperCase()) + "Changed"
   if (name in this) this[name](from, to)
}
Enter fullscreen mode Exit fullscreen mode

Then I can just add methods like xIntervalChanged(from, to) { /* ... */ } instead of having a long if in a single method.

Collapse
 
dannyengelman profile image
Danny Engelman • Edited

Same for Events:

.addEventListener("click",this)
Enter fullscreen mode Exit fullscreen mode
handleEvent(evt) { // standard method on every Object
  let method = "event_" + evt.type;
  this[method] && this[method](evt)
}
Enter fullscreen mode Exit fullscreen mode
event_click(evt) { }
Enter fullscreen mode Exit fullscreen mode