Przejdź do głównej treści
Grafika przedstawia ukryty obrazek

Jak zbudować reaktywny interfejs UI bez frameworków

Zdjecie zwiazane z Jak zbudowa reaktywny interfejs UI bez frameworkw

Współczesny frontend zdominowany jest przez frameworki takie jak React, Vue czy Angular. Często jednak zapominamy, że stoją za nimi proste idee architektoniczne, które można zaimplementować samodzielnie – nawet w kilkudziesięciu liniach kodu.

W tym artykule zbudujemy mini framework MVVM w czystym JavaScript, który pozwala tworzyć dynamiczne interfejsy UI bez żadnych zależności.

Czym jest MVVM?

MVVM (Model–View–ViewModel) to wzorzec architektoniczny, który rozdziela:

  • Model – dane aplikacji
  • View – HTML (interfejs użytkownika)
  • ViewModel – logika prezentacji i stan UI

Kluczową ideą MVVM jest to, że:

Widok nie manipuluje bezpośrednio DOM-em — zamiast tego wiąże się z danymi.

Cel mini frameworka

Zbudujemy mechanizm, który zapewni:

  • reaktywne dane (observable)
  • deklaratywne wiązania (data-bind)
  • two-way binding (input ↔ dane)
  • automatyczne aktualizacje UI
  • zero zależności zewnętrznych

Struktura aplikacji

Całość mieści się w jednym pliku HTML:

  • HTML → View
  • JavaScript → Mini framework + ViewModel

Widok (HTML)

Zacznijmy od widoku, który nie zawiera żadnej logiki JS:

<div id="app" class="box">
  <h2 data-bind="text: title"></h2>

  <input type="text" placeholder="Wpisz tekst"
         data-bind="value: name">

  <p>
    Aktualna wartość:
    <strong data-bind="text: name"></strong>
  </p>

  <button data-bind="click: addItem">
    Dodaj do listy
  </button>

  <div class="box">
    <h4>Lista:</h4>
    <div data-bind="foreach: items">
      •
    </div>
  </div>
</div>

Co tu się dzieje?

Zamiast manipulować DOM-em w JS, używamy atrybutu:

data-bind="typ: właściwość"

Przykłady:

  • text: title → tekst elementu pochodzi z title
  • value: name → input jest powiązany z danymi
  • click: addItem → obsługa zdarzenia
  • foreach: items → renderowanie listy

Observable – serce reaktywności

Podstawą frameworka jest funkcja observable.

function observable(initial) {
  let _value = initial;
  const subscribers = new Set();

  function obs(newValue) {
    if (arguments.length) {
      _value = newValue;
      subscribers.forEach(fn => fn(_value));
    }
    return _value;
  }

  obs.subscribe = fn => subscribers.add(fn);
  return obs;
}

Jak działa observable?

  • przechowuje wartość (_value)
  • działa jak getter i setter
  • informuje subskrybentów o zmianach
const name = observable('Jan');

name();        // getter
name('Anna');  // setter

To dokładnie ten sam koncept, który stoi za:

  • Knockout.js
  • Vue 2
  • sygnałami (signals)

Mechanizm wiązań – applyBindings

Funkcja applyBindings łączy ViewModel z View.

function applyBindings(viewModel, root = document.body) {
  const elements = root.querySelectorAll('[data-bind]');

Dla każdego elementu z data-bind:

  1. odczytujemy deklarację
  2. rozbijamy ją na typ i nazwę właściwości
  3. podpinamy odpowiednią logikę

Implementacja bindingów

text

el.textContent = prop();
prop.subscribe(v => el.textContent = v);
  • ustawia tekst początkowy
  • aktualizuje DOM przy każdej zmianie danych

value (two-way binding)

el.value = prop();
prop.subscribe(v => el.value = v);

el.addEventListener('input', e =>
  prop(e.target.value)
);

Efekt:

  • zmiana danych → input się aktualizuje
  • zmiana inputa → dane się aktualizują

click

el.addEventListener('click',
  prop.bind(viewModel)
);
  • metoda ViewModel
  • kontekst this ustawiony poprawnie

foreach

const render = items => {
  el.innerHTML = '';
  items.forEach(item => {
    const node = document.createElement('div');
    node.textContent = item;
    el.appendChild(node);
  });
};
  • obserwuje tablicę
  • renderuje listę od nowa przy każdej zmianie

ViewModel – logika aplikacji

const vm = {
  title: MiniMVVM.observable('Mini MVVM – JavaScript'),
  name: MiniMVVM.observable(''),
  items: MiniMVVM.observable([]),

  addItem() {
    if (!this.name()) return;
    this.items([...this.items(), this.name()]);
    this.name('');
  }
};

Co tu mamy?

  • stan aplikacji
  • logikę biznesową
  • brak bezpośrednich operacji na DOM

Uruchomienie aplikacji

MiniMVVM.applyBindings(vm, document.getElementById('app'));

Jedna linia:

  • skanuje HTML
  • podłącza dane
  • uruchamia reaktywność

Co zyskaliśmy?

  • czytelny podział odpowiedzialności
  • deklaratywny HTML
  • automatyczne aktualizacje UI
  • zero frameworków
  • pełna kontrola nad kodem

Ograniczenia (świadomie)

Ten mini framework:

  • nie ma virtual DOM
  • nie ma computed
  • nie ma komponentów

I bardzo dobrze — jego celem jest edukacja, nie zastąpienie Vue czy Reacta.

Działający przykład

Aktualna wartość:

Lista:

    Kompletny kod HTML

    <!DOCTYPE html>
    <html lang="pl">
    <head>
      <meta charset="UTF-8">
      <title>Mini MVVM Framework</title>
      <style>
        body {
          font-family: Arial, sans-serif;
          padding: 30px;
        }
    
        input, button {
          padding: 6px;
          margin: 5px 0;
        }
    
        button {
          cursor: pointer;
        }
    
        .box {
          margin-top: 15px;
          padding: 10px;
          border: 1px solid #ccc;
        }
      </style>
    </head>
    <body>
    
      <div id="app" class="box">
        <h2 data-bind="text: title"></h2>
    
        <input type="text" placeholder="Wpisz tekst"
               data-bind="value: name">
    
        <p>Aktualna wartość: <strong data-bind="text: name"></strong></p>
    
        <button data-bind="click: addItem">Dodaj do listy</button>
    
        <div class="box">
          <h4>Lista:</h4>
          <div data-bind="foreach: items">
            •
          </div>
        </div>
      </div>
    
      <script>
        // ===== MiniMVVM Framework =====
        const MiniMVVM = (() => {
    
          function observable(initial) {
            let _value = initial;
            const subscribers = new Set();
    
            function obs(newValue) {
              if (arguments.length) {
                _value = newValue;
                subscribers.forEach(fn => fn(_value));
              }
              return _value;
            }
    
            obs.subscribe = fn => subscribers.add(fn);
            return obs;
          }
    
          function applyBindings(viewModel, root = document.body) {
            const elements = root.querySelectorAll('[data-bind]');
    
            elements.forEach(el => {
              const bindings = el.dataset.bind.split(',');
    
              bindings.forEach(binding => {
                const [type, key] = binding.split(':').map(s => s.trim());
                const prop = viewModel[key];
    
                if (!prop) return;
    
                // text binding
                if (type === 'text') {
                  el.textContent = prop();
                  prop.subscribe(v => el.textContent = v);
                }
    
                // value binding (two-way)
                if (type === 'value') {
                  el.value = prop();
                  prop.subscribe(v => el.value = v);
                  el.addEventListener('input', e => prop(e.target.value));
                }
    
                // click binding
                if (type === 'click') {
                  el.addEventListener('click', prop.bind(viewModel));
                }
    
                // visible binding
                if (type === 'visible') {
                  el.style.display = prop() ? '' : 'none';
                  prop.subscribe(v => el.style.display = v ? '' : 'none');
                }
    
                // foreach binding
                if (type === 'foreach') {
                  const template = el.innerHTML;
                  el.innerHTML = '';
    
                  const render = items => {
                    el.innerHTML = '';
                    items.forEach(item => {
                      const node = document.createElement('div');
                      node.textContent = item;
                      el.appendChild(node);
                    });
                  };
    
                  render(prop());
                  prop.subscribe(render);
                }
              });
            });
          }
    
          return { observable, applyBindings };
        })();
    
        // ===== ViewModel =====
        const vm = {
          title: MiniMVVM.observable('Mini MVVM – JavaScript'),
          name: MiniMVVM.observable(''),
          items: MiniMVVM.observable([]),
    
          addItem() {
            if (!this.name()) return;
            this.items([...this.items(), this.name()]);
            this.name('');
          }
        };
    
        // ===== Start =====
        MiniMVVM.applyBindings(vm, document.getElementById('app'));
      </script>
    
    </body>
    </html>
    

    Podsumowanie

    Ten przykład pokazuje, że:

    nowoczesne UI to idea, nie biblioteka

    Zrozumienie MVVM i reaktywności:

    • pomaga pisać lepszy kod
    • ułatwia debugowanie frameworków
    • daje kontrolę nad architekturą aplikacji
    8 stycznia 2026 3

    Kategorie

    programowanie

    Dziękujemy!
    ()

    Powiązane wpisy


    Informacja o cookies

    Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.