UNPKG

joicomponents

Version:

Design patterns to build native web components

102 lines (87 loc) 4.83 kB
# HowTo: `.attributeChangedCallback(...)` `attributeChangedCallback(...)` is a callback method that is triggered every time an *observed* attribute is instantiated or changed. To *observe* an attribute, its name must be added as a string to the returned array from the native function `static get observedAttributes()`. For more about `static get observedAttributes()`, see the [StaticSettings pattern](../chapter2_HowToMakeMixins/Pattern3_StaticSettings.md). ## Why `static get observedAttributes()`? Or, why not always trigger an `attributeChangedCallback(...)` for all attributes? First. When you make a web component, you only need to process changes to a few custom attributes. If not, you are doing something wrong. Second. HTML elements often have many attributes, such as `style` and `class`. Some of these attributes can change value quite often. So, if all attribute changes triggered a JS callback, the browser would have to do lots of unnecessary work. Therefore, the browser is interested in *avoiding* `attributeChangedCallback(...)` for as many attribute changes as possible. And it skips all attribute changes that is not explicitly observed via `static get observedAttributes()`. ## Example SayMyName ```html <script> class SayMy extends HTMLElement { static get observedAttributes(){ return ["first-name", "last-name"]; //[1] } constructor(){ super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = `<span>your name is: </span> <span id='lastName'></span>, <span id='firstName'></span>`; } attributeChangedCallback(name, oldValue, newValue) { if (name === "first-name"){ //[2] const span = this.shadowRoot.querySelector("span#firstName"); span.innerText = newValue; //[3] } else if (name === "last-name"){ //[2] const span = this.shadowRoot.querySelector("span#lastName"); span.innerText = newValue; //[3] } } } customElements.define("say-my", SayMy); </script> <say-my first-name="Ivan" last-name="the Great"></say-my> <!--4--> <script> setTimeout(function(){ const el = document.querySelector("say-my"); el.setAttribute("last-name", "the Terrible"); //[5] }, 3000); </script> ``` 1. The `static get observedAttributes()` function returns an array of attribute names that the element should observe. Here, both `first-name` and `last-name` are added to the returned array. 2. Inside `attributeChangedCallback(name, oldValue, newValue)` the attribute name is used to identify which attribute the callback concerns. 3. The `oldValue` and `newValue` are used inside the custom element. Here, the new values of the name attributes are added as text in `<say-my>`'s shadowDOM. 4. When the attributes are ascribed values for the first time, `attributeChangedCallback(...)` is called twice: 1. `attributeChangedCallback("first-name", undefined, "Ivan")` 2. `attributeChangedCallback("last-name", undefined, "the Great")` After these callbacks, the browser presents `your name is: the Great, Ivan`. 5. After 3000ms the `last-name` attribute is changed. This change triggers a new callback: 1. `attributeChangedCallback("last-name", "the Great", "the Terrible")` After this callback, the browser presents `your name is: the Terrible, Ivan`. ## When is `attributeChangedCallback(...)` triggered? `attributeChangedCallback(...)` is triggered immediately, synchronously. But, as with other [custom element reactions](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-reactions), `.attributeChangedCallback()` can be 'grouped' and run at the end *within* functions that manipulate DOM for several elements at the same time. Examples of such functions are `.cloneNode`, `.innerHTML` and `.appendChild`/`.removeChild`. When these functions run, they affect a whole group of children/elements within a single call. *Within* such functions, `.attributeChangedCallback()`, `connectedCallback()` etc. for several children are grouped together. ## Opinion on `attributeChangedCallback(...)` parameter sequence There is a minor flaw with the `attributeChangedCallback("name", "newValue", "oldValue")` signature: `oldValue` is listed before `newValue`. `newValue` is commonly needed, while `oldValue` is not. If `oldValue` had been positioned last, then many `attributeChangedCallback(...)` implementations could have skipped the `oldValue` argument. But, what's done is done. Can't put toothpaste back in the tube. ## References * MDN on `attributeChangedCallback` * MDN on `observedAttributes`