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.