Jak zbudować reaktywny interfejs UI bez frameworków
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 ztitlevalue: name→ input jest powiązany z danymiclick: addItem→ obsługa zdarzeniaforeach: 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:
- odczytujemy deklarację
- rozbijamy ją na typ i nazwę właściwości
- 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
thisustawiony 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