Jak zrealizować dwukierunkowe wiązanie danych MVVM w Vanilla JS?
Wzorzec projektowy MVVM (Model-View-ViewModel) umożliwia rozdzielenie logiki aplikacji od jej warstwy prezentacji. W kontekście aplikacji webowych pomaga zarządzać interakcjami użytkownika i aktualizacjami danych w sposób czytelny oraz utrzymywany. W odróżnieniu od popularnych frameworków takich jak Vue.js czy Angular, możemy wdrożyć ten wzorzec również za pomocą czystego JavaScriptu (Vanilla JS).
Główne elementy MVVM
- Model — Reprezentuje dane oraz logikę biznesową aplikacji.
- View — Odpowiada za interfejs użytkownika (HTML).
- ViewModel — Pośrednik między Modelem a Widokiem, zarządzający synchronizacją danych i zdarzeniami.
Poniżej przedstawię krok po kroku implementację tego wzorca w Vanilla JS.
1. Definicja modelu
Model będzie prostym obiektem przechowującym dane aplikacji. Dodamy do niego funkcjonalność subskrypcji i powiadamiania obserwatorów o zmianach danych:
class Model {
constructor(initialData = {}) {
this.data = initialData;
this.listeners = [];
}
subscribe(listener) {
this.listeners.push(listener);
}
notify() {
this.listeners.forEach(listener => listener(this.data));
}
set(property, value) {
if (this.data[property] !== value) {
this.data[property] = value;
this.notify();
}
}
get(property) {
return this.data[property];
}
}
2. Tworzenie ViewModelu
ViewModel zarządza komunikacją między Modelem a Widokiem. Będzie on subskrybował zmiany Modelu i aktualizował Widok.
class ViewModel {
constructor(model) {
this.model = model;
this.bindings = {};
}
bind(property, element, eventType = null) {
// Aktualizacja View na zmiany Modelu
element.value = this.model.get(property) || "";
element.textContent = this.model.get(property) || "";
this.model.subscribe(data => {
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
element.value = data[property];
} else {
element.textContent = data[property];
}
});
// Aktualizacja Modelu na zmiany View (dwukierunkowe wiązanie)
if (eventType) {
element.addEventListener(eventType, e => {
this.model.set(property, e.target.value);
});
}
}
}
3. Implementacja Widoku
HTML może zawierać elementy, które będziemy wiązać z Modelem:
<div>
<label for="name">Name:</label>
<input type="text" id="name">
</div>
<p>Name: <span id="displayName"></span></p>
4. Integracja komponentów
// Inicjalizacja Modelu
const model = new Model({ name: 'John Doe' });
// Inicjalizacja ViewModelu
const viewModel = new ViewModel(model);
// Powiązanie danych
const nameInput = document.getElementById('name');
const displayName = document.getElementById('displayName');
viewModel.bind('name', nameInput, 'input');
viewModel.bind('name', displayName);
5. Gotowy HTML z JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vanilla MVVM</title>
</head>
<body>
<div>
<label for="name">Name:</label>
<input type="text" id="name">
</div>
<p>Name: <span id="displayName"></span>
</p>
<script>
class Model {
constructor(initialData = {}) {
this.data = initialData;
this.listeners = [];
}
subscribe(listener) {
this.listeners.push(listener);
}
notify() {
this.listeners.forEach(listener => listener(this.data));
}
set(property, value) {
if (this.data[property] !== value) {
this.data[property] = value;
this.notify();
}
}
get(property) {
return this.data[property];
}
}
class ViewModel {
constructor(model) {
this.model = model;
this.bindings = {};
}
bind(property, element, eventType = null) {
// Aktualizacja View na zmiany Modelu
element.value = this.model.get(property) || "";
element.textContent = this.model.get(property) || "";
this.model.subscribe(data => {
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
element.value = data[property];
} else {
element.textContent = data[property];
}
});
// Aktualizacja Modelu na zmiany View (dwukierunkowe wiązanie)
if (eventType) {
element.addEventListener(eventType, e => {
this.model.set(property, e.target.value);
});
}
}
}
// Inicjalizacja Modelu
const model = new Model({
name: 'John Doe'
});
// Inicjalizacja ViewModelu
const viewModel = new ViewModel(model);
// Powiązanie danych
const nameInput = document.getElementById('name');
const displayName = document.getElementById('displayName');
viewModel.bind('name', nameInput, 'input');
viewModel.bind('name', displayName);
</script>
</body>
</html>
6. Działający przykład
Name:
7. Rozszerzenia
Możesz rozbudować powyższą implementację o:
- Obsługę walidacji danych w ViewModelu.
- Większą liczbę dwukierunkowych wiązań.
- Obsługę dynamicznego dodawania elementów do DOM.
- Lepsze zarządzanie wydajnością przez debouncing przy wiązaniu zdarzeń.
Podsumowanie
Implementacja wzorca MVVM w Vanilla JS wymaga ręcznego zarządzania subskrypcjami i synchronizacją danych, ale daje pełną kontrolę nad logiką aplikacji. Wdrożenie tego wzorca może być świetnym ćwiczeniem pozwalającym zrozumieć, jak działają popularne frameworki frontendowe.