Staramy się pisać bardziej modularny kod, bazując na przykładzie z poprzednich lekcji, który sobie szybko przypomnimy. Do dzieła.
Ok, nasz komponent ReactiveVar:
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);
Nasz komponent jest reaktywny, możemy zmieniać w devtoolsach strzałkami jego atrybut i widzieć od razu reakcję (trzeba wciskać enter, bo to shadow DOM).
Potrzebuje jednak połączenia z reaktywnymi zmiennymi:
function ReactiveVariable(value, id, ...subscibedIds){
this.value = value;
this.id = id;
this.subscibedIds = [...subscibedIds];
}
Funkcja konstruktor, bierze value, id, oraz opcjonalnie id innych, które się subskrybują do tego. Teraz funkcja update:
ReactiveVariable.prototype.update = function(newVal){
document
.getElementById(this.id)
.setAttribute('value', newVal);
this.subscibedIds.forEach(function(id){
document
.getElementById(id)
.setAttribute('value', newVal);
});
}
Oraz subscribe, unsubscribe, łatwizna:
ReactiveVariable.prototype.subscribe = function(id){
this.subscibedIds.push(id);
}
ReactiveVariable.prototype.unsubscribe = function(id){
this.subscibedIds = this.subscibedIds.
filter((val) => val !== id);
}
Reaktywna zmienna z proxy:
let cnt = new ReactiveVariable(0, 'reactiveVar', 'numIpt');
const cntHandler = {
set(obj, prop, value){
if(prop !== 'value')
return Reflect.set(...arguments);
obj.update(value);
return Reflect.set(...arguments);
}
}
let cntProxy = new Proxy(cnt, cntHandler);
No i reszta kodu:
console.log(cntProxy);
cntProxy.value++;
console.log(cntProxy);
let buttons = document.querySelectorAll("#counter button");
buttons.forEach(function(btn){
btn.addEventListener("click", function(e){
if(e.target.matches("#incBtn"))
return cntProxy.value++;
if(e.target.matches("#decBtn"))
return cntProxy.value--;
if(e.target.matches("#resBtn"))
return cntProxy.value = 0;
});
});
Ok, tworzymy nowy HTML:
<!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>
<input type="number" name="numIpt" id="numIpt" readonly>
</div>
<script src="./reactive5.js" type="module"></script>
</body>
</html>
Type module, raczej już to znamy. Tworzymy też plik usereactive.js:
function ReactiveVariable(value, id, ...subscibedIds){
this.value = value;
this.id = id;
this.subscibedIds = [...subscibedIds];
}
ReactiveVariable.prototype.update = function(newVal){
document
.getElementById(this.id)
.setAttribute('value', newVal);
this.subscibedIds.forEach(function(id){
document
.getElementById(id)
.setAttribute('value', newVal);
});
}
ReactiveVariable.prototype.subscribe = function(id){
this.subscibedIds.push(id);
}
ReactiveVariable.prototype.unsubscribe = function(id){
this.subscibedIds = this.subscibedIds.
filter((val) => val !== id);
}
To zostawiamy, jako niepodlegające eksportowi. Teraz pora napisać funkcję useReactive:
export default function useReactive(value, id, ...subscibedIds){
let variable = new ReactiveVariable(value, id, ...subscibedIds);
let variableHanlder = {
set(obj, prop, value){
if(prop !== 'value')
return Reflect.set(...arguments);
obj.update(value);
return Reflect.set(...arguments);
}
}
let variableProxy = new Proxy(variable, variableHanlder);
return [variable, variableProxy];
}
Od razu mówię, nie jest to odpowiednik useState z Reacta. Raczej taka luźna wariacja, w końcu tam w kodzie też mieliśmy cnt i cntProxy, ok teraz import i użycie:
import useReactive from "./usereactive.js";
let [cnt, cntProxy] = useReactive(0, 'reactiveVar', 'numIpt');
console.log(cntProxy);
cntProxy.value++;
console.log(cntProxy);
let buttons = document.querySelectorAll("#counter button");
buttons.forEach(function(btn){
btn.addEventListener("click", function(e){
if(e.target.matches("#incBtn"))
return cntProxy.value++;
if(e.target.matches("#decBtn"))
return cntProxy.value--;
if(e.target.matches("#resBtn"))
return cntProxy.value = 0;
});
});
Bardzo uprościliśmy i zautomatyzowaliśmy proces tworzenia tych reaktywnych zmiennych. Więcej JS już niedługo.