Tworzymy Web Component, jakim jest reaktywna zmienna – koncept, który powinniśmy znać już z pierwszych lekcji o frameworkach frontendowych. Do dzieła.

Ok, co chcemy osiągnąć:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./reactivevar.js"></script>
</head>
<body>
    <p>
        <reactive-variable pref="Counter: " value="0"></reactive-variable>
        <br>
        <reactive-variable pref="FontSize : " value="0" suff="px"></reactive-variable>
        
    </p>
    <p>
        <reactive-variable pref="FontSize : " value="0" suff="px"></reactive-variable>
    </p>
</body>
</html>

Swoją drogą od razu uwrażliwię na oczywiste ograniczenia web componentów:

  • Nie mogą być samozamykające się, a jeżeli są, to wstawienie kilku pod rząd jako rodzeństwo może sprawić, że nieprawidłowo będą wyświetlane (będzie autozamykanie)
  • Nazwy atrybutów nie mogą przesłaniać nazw atrybutów z HTMLElement, z którego dziedziczymy (np. prefix, suffix)
  • Będziemy operować w shadow domie, zatem atrybuty będą mogły się zmieniać, ale zapomnijmy o takim komforcie jak zmienianie atrybutów strzałkami w devtoolsach – to robić możemy, ale by zaobserwować zmianę, musimy wcisnąć enter

Ok, tworzymy naszą klasę:

class ReactiveVar extends HTMLElement {
    
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <span></span>
        `;
        this.pref = null;
        this.suff = null;
        
      }

}

customElements.define('reactive-variable', ReactiveVar);

To chyba oczywiste. Teraz metoda pomocnicza, która skleja prefix, suffix i treść do siebie:

_getNewTextValue(){
        let prefix = this.pref ?? '';
        let suffix = this.suff ?? '';
        let value = this.getAttribute('value');
        return `${prefix}${value}${suffix}`;
    }

Ok, teraz connectedCallback, metoda, która ruszy po podłączeniu elementu:

connectedCallback(){
        if(this.hasAttribute("pref")){
            this.pref = this.getAttribute("pref");
        }
        if(this.hasAttribute("suff")){
            this.suff = this.getAttribute("suff");
        }

        if (this.hasAttribute('value')) {
            const span = this.shadowRoot.querySelector('span');
            span.textContent = this._getNewTextValue();
          }
          
    }

Czyli co robimy:

  • sprawdzamy, czy jest atrybut pref i dopisujemy jego wartość
  • sprawdzamy, czy jest atrybut suff i dopisujemy jego wartość
  • sprawdzamy, czy jest value i wpisujemy jako textContent do naszego spana z shadow roota korzystając z metody pomocniczej

Swoją drogą jeżeli lubimy tak jak ja czytać sobie do poduszki dokumentację MDN to możemy przypomnieć sobie, że klasa Node ma taką property isConnected:

let test = document.createElement("p");
console.log(test.isConnected); // Returns false
document.body.appendChild(test);
console.log(test.isConnected); // Returns true

To powinno nam nieco rozjaśnić, czym jest connectedCallback. Ok, teraz obsługa zmiany atrybutów – tylko value chcemy obserwować:

attributeChangedCallback(name, oldValue, newValue) {
        if(oldValue === newValue)
            return;
        if(name === "value"){
            this.shadowRoot.querySelector('span').textContent = this._getNewTextValue();
        }
      }
    
      static get observedAttributes() {
        return ['value'];
      }

I tylko value obserwujemy (statyczna metoda observedAttributes). Sam observer sprawdza, czy wartości się od siebie różnią, jeżeli tak, to wykonuje getNewTextValue.

Chyba proste, mam nadzieję, że coraz lepiej web komponenty ogarniamy.