Tak, Countery to takie hello-worldy w bibliotece React, ale prawdziwa zabawa to napisać Countera bez Reacta. I to właśnie zrobimy, do dzieła.

Ok, po pierwsze z poprzedniej lekcji potrzebujemy komponent:

class ReactiveVar extends HTMLElement {

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

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

    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();
          }
          
    }

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

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

Mam nadzieję, że wszystko jasne, w tym:

  • Dlaczego używamy super w konstruktorze (bo dziedziczymy z HTMLElement)
  • Dlaczego dziedziczymy z HTMLELement (bo ma metodę attachShadow)
  • Dlaczego nie możemy używać z atrybutów prefix/suffix (bo dziedziczymy z HTMLElement i nie przysłaniamy jego własnych)
  • Dlaczego możemy robić overshadow dla value (bo NIE dziedziczymy z HTMLInputElement, więc niczego nie przysłaniamy)
  • Co to jest ta metoda pomocnicza (to jest dokładnie to, na co wygląda)
  • Co to jest connectedCallback (to takie coś, co jest raz odpalane, gdy Node.isConnected z false zmienia się na true)
  • Co to jest attributeChangedCallback i dlaczego zmiana jest rejestrowana nie w momencie klikania strzałkami w devtoolsach ale w momencie wciśnięcia entera (bo używamy shadow doma)
  • Co to jest observedAttributes i co ono oznacza
  • Dlaczego w „czystych” web componentach nie jest dobrym pomysłem używać samozamykających się (bo przeglądarka głupieje i sama domyka, psując markup reszty, to nie React, który wie jak z tym postępować)

Ok, wszystko jasne? To przypomnijmy sobie ten kawałek kodu o zmiennych reaktywnych:

let name = {
    name: "John"
};
let fullname = name.name + " Doe";

const nameHandler = {
    set(obj, prop, value){
        fullname = value + " Doe"; 
    }
}

let nameProxy = new Proxy(name, nameHandler);


console.log(fullname);
//John Doe

nameProxy.name = "Jane";

console.log(fullname);
//Jane Doe

Ok, jak przypomnianie to tworzymy markup:

<!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>
    <div id="counter">
        <reactive-variable 
        id="reactiveVar" 
        pref="Counter: " 
        value="0"> </reactive-variable>
        <button id="incBtn">+</button>
        <button id="decBtn">-</button>
        <button id="resBtn">Reset</button>
    </div>
    <script src="./reactive2.js" defer></script>
    
</body>
</html>

Ważne:

  • W sekcji head zaimportowaliśmy plik js z naszym komponentem
  • W sekcji body wrzuciliśmy nasz skrypt, który teraz będziemy pisać, z atrybutem defer – asynchronicznie ładuj plik, poczekaj z egzekucją kodu aż wszystko gotowe

Ok, piszemy nasz obiekt cnt:

let cnt = {
    value: 0,
    id: 'reactiveVar'
};

Taki to będzie szablon – value wartość oraz id elementu, który ma być zmieniany. Okej, teraz handler:

const cntHandler = {
    set(obj, prop, value){
        if(prop !== 'value') 
            return Reflect.set(...arguments);
        document.getElementById(obj.id)
        .setAttribute('value', value);
        return Reflect.set(...arguments);
    }
}

Czyli np. id możemy zmieniać bez żadnych ceregieli, ale już jak zmieniamy value, to odszukujemy w dokumencie obiekt o takim id i ustawiamy mu value na nową wartość.

No to teraz robimy proxy plus dopisujemy tam coś:

let cntProxy = new Proxy(cnt, cntHandler);

console.log(cntProxy); //0

cntProxy.value++;

console.log(cntProxy); //1

No i proszę, w HTML też się zmieniło. Nowe value zostało przekazane do atrybutu value naszego custom componentu, który od tego już przejął inicjatywę.

Normalnie reaktywność bez Reacta. Łał…

Okej, teraz event listener:

let incBtn = document.querySelector("#incBtn");

incBtn.addEventListener("click", function(e){

    cntProxy.value++;
});

Lekcja się robi przydługa, więc reszta w następnej, plus ciekawy patent związany z delegacją w stylu jeszcze nam nieznanym.