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.