W poprzedniej lekcji rozłożyłeś moduł
.vue na trzy sekcje i zobaczyłeś, jak szablon, logika i style mieszczą się w jednym pliku. Panel podtrzymywania życia, który tam powstał, miał jednak poważną wadę: był martwy. Pokazywał jeden odczyt i nic więcej - żadnego przycisku, żadnej reakcji, żadnej zmiany w czasie.W Centrum Kontroli NOVA LAB taki panel nadaje się co najwyżej na tabliczkę przy drzwiach. Dyżurny operator musi móc nacisnąć przycisk, przestawić fazę misji, zobaczyć, jak dystans do Marsa maleje, i w tej samej sekundzie wychwycić moment, w którym zielony napis zmienia się w żółty alert. Dziś, @name, zbudujesz swój pierwszy panel, który to potrafi - panel statusu misji MARS ONE.
Po drodze poznasz cztery narzędzia i to one są prawdziwym tematem tej lekcji: funkcję
ref(), interpolację {{ }}, obsługę zdarzeń przez @click oraz dynamiczne klasy :class. Każde z nich zobaczysz najpierw jako brak, który boli, a dopiero potem jako rozwiązanie.Zacznijmy od najprostszej możliwej wersji panelu - takiej, jaką dałoby się napisać w czystym HTML-u, bez żadnego frameworka. Nazwa misji i liczba dni pozostałych do celu są wpisane wprost w znaczniki.
1<template>
2 <h1>MARS ONE - Pierwsza Kolonia</h1>
3 <p>Dni do celu: 180</p>
4</template>Ten kod jest poprawny i wyświetli się bez zarzutu. Problem pojawia się dopiero następnego ranka: telemetria melduje 179 dni, a jedynym sposobem na aktualizację panelu jest otwarcie pliku, poprawienie liczby i ponowne wgranie systemu. Wartość jest przyklejona do znacznika. Vue nie ma tu nic do roboty, bo nie wie, że liczba 180 znaczy cokolwiek więcej niż fragment tekstu.
Żeby cokolwiek mogło się zmieniać samo, wartość musi najpierw przenieść się z szablonu do sekcji
<script setup> i dostać opiekuna, który zauważy każdą jej zmianę.Vue udostępnia do tego funkcję
ref. Sprowadzasz ją instrukcją import z pakietu vue i wywołujesz, podając wartość początkową. W zamian dostajesz kontener - pojedynczy pojemnik, w którym siedzi Twoja wartość i który Vue trzyma na oku. Nazwa bierze się od angielskiego słowa reference: to nie jest sama liczba, tylko odnośnik do miejsca, w którym ta liczba mieszka.Ta jedna cecha - bycie obserwowanym - jest całą różnicą. Gdy podmienisz zawartość kontenera, Vue o tym wie, odnajduje wszystkie miejsca w szablonie, które z niej korzystają, i odświeża dokładnie je. Nie całą stronę, nie cały panel, tylko te fragmenty. To właśnie nazywamy reaktywnością i to jest serce Vue.
1<script setup>
2import { ref } from 'vue'
3
4const missionName = ref('MARS ONE - Pierwsza Kolonia')
5const daysRemaining = ref(180)
6</script>Wartości są identyczne jak przed chwilą - to się nie zmieniło. Zmieniło się miejsce, w którym mieszkają, i to, że Vue o nich wie. Zwróć uwagę na kształt takiej linii, bo napiszesz ją jeszcze setki razy: najpierw słowo
const, potem nazwa zmiennej, potem znak =, a na samym końcu wywołanie ref() z wartością początkową w nawiasie. Cztery części, zawsze w tej kolejności - const counter = ref(0) wygląda dokładnie tak samo.Słowo
const przy wartości, która ma się zmieniać, wygląda na pomyłkę, ale nią nie jest. const blokuje podmianę samego kontenera i bardzo dobrze - gdybyś podstawił pod tę nazwę coś zupełnie innego, Vue straciłoby połączenie z szablonem. Zawartość kontenera wolno zmieniać do woli i za chwilę będziesz to robił.Od razu uwaga porządkowa, bo
ref() bywa mylone z zupełnie innymi narzędziami. Ta funkcja służy wyłącznie do tworzenia reaktywnych danych. Nie ma nic wspólnego z nawigacją między ekranami stacji - tym zajmuje się Vue Router z tabeli ekosystemu, którą widziałeś w pierwszej lekcji. Nie stylizuje komponentów - za wygląd odpowiada sekcja <style scoped> z lekcji poprzedniej. Nie służy też do wczytywania plików - pliki i pakiety sprowadzasz instrukcją import, tak jak przed chwilą sprowadziłeś samo ref z pakietu vue.Dane są już w skrypcie, ale szablon wciąż ich nie widzi. Mostem między jedną sekcją a drugą jest interpolacja: podwójny nawias klamrowy, który w świecie Vue nazywa się wąsami. Wpisujesz w niego nazwę zmiennej, a Vue wstawia w tym miejscu jej aktualną wartość - i pilnuje, żeby po każdej zmianie wstawić ją ponownie.
1<template>
2 <h1>{{ missionName }}</h1>
3 <p>Dni do celu: {{ daysRemaining }}</p>
4</template>Ta sama nazwa misji, ta sama liczba na ekranie - ale teraz zarządza nimi Vue. Zauważ, czego nie musiałeś zmieniać: znaczniki
<h1> i <p> zostały zwykłym HTML-em, napis "Dni do celu" nadal jest zwykłym tekstem, a wąsy zajęły dokładnie to miejsce, w którym wcześniej siedziała wpisana ręcznie liczba. Vue nie zabiera Ci HTML-a, tylko dokłada do niego miejsca, w które może wstawić wartość.Wewnątrz wąsów wolno umieścić nie tylko nazwę zmiennej, ale każde wyrażenie JavaScript, które coś zwraca: działanie arytmetyczne, wywołanie metody, warunek. Nie zmieścisz tam natomiast instrukcji sterujących w rodzaju
if ani pętli - to nie jest miejsce na logikę, tylko na jej wynik.Jedno ograniczenie zapamiętaj od razu, bo wrócimy do niego przy stylowaniu panelu: wąsy działają w treści tekstowej elementu, a nie w wartości atrybutu. Zapis
class="{{ statusClass }}" nie podstawi żadnej wartości, bo do atrybutów prowadzi w Vue zupełnie inna droga.Skoro
ref() zwraca kontener, a nie samą wartość, to jak dostać się do środka? Przez właściwość .value. I tu pojawia się jedyna reguła Vue, o której początkujący zapominają najczęściej.W sekcji
<script setup> pracujesz na kontenerze, więc .value piszesz zawsze. W szablonie Vue rozpakowuje kontener za Ciebie, więc .value nie piszesz nigdy. Dlatego w bloku powyżej wystarczyło {{ daysRemaining }} - zapis {{ daysRemaining.value }} byłby w szablonie błędem.1<script setup>
2import { ref } from 'vue'
3
4const daysRemaining = ref(180)
5
6// caly kontener, nie liczba
7console.log(daysRemaining)
8
9// wartosc ze srodka kontenera
10console.log(daysRemaining.value)
11
12// podmiana wartosci w kontenerze
13daysRemaining.value = 179
14</script>Pierwsze
console.log wypisze obiekt opakowujący; jego dokładny kształt zależy od wersji Vue, więc nie przywiązuj się do tego, co zobaczysz - ważne, że to nie jest liczba. Drugie wypisze 180, czyli zawartość kontenera. Najciekawsza jest ostatnia linia: podmienia wartość w kontenerze, a nie sam kontener. Zmienna daysRemaining wskazuje wciąż na ten sam obiekt, powiązanie z szablonem trzyma się bez zmian, a mimo to liczba na ekranie zmieni się sama.Panel wyświetla już dane, ale operator nie ma czym w niego kliknąć. Potrzebujesz przycisku, który po naciśnięciu uruchomi Twój kod - przeglądarka nazywa takie naciśnięcie zdarzeniem
click.Vue podpina się pod zdarzenia dyrektywą
v-on. Piszesz v-on:click, a po znaku równości podajesz nazwę funkcji, którą Vue ma wywołać. Ten zapis jest tak częsty, że dostał skrót w postaci małpy: @click znaczy dokładnie to samo co v-on:click. W NOVA LAB używamy wyłącznie skróconej formy i to ją polecam - jest krótsza, a w gęstym szablonie od razu widać, w którym miejscu coś się dzieje.Nazwę funkcji podajesz bez nawiasów. Zapis
@click="advanceDay" znaczy: oto funkcja, wywołaj ją, gdy ktoś kliknie. Sama funkcja to najzwyklejszy JavaScript, napisany w <script setup> obok zmiennych, a wewnątrz niej obowiązuje reguła z poprzedniej sekcji, więc czytasz i zapisujesz przez .value. Użyjemy jeszcze wbudowanej funkcji Math.max, która zwraca największą z podanych jej wartości - dzięki niej licznik dni zatrzyma się na zerze, zamiast zejść poniżej.1<template>
2 <p>Dni do celu: {{ daysRemaining }}</p>
3 <button @click="advanceDay">Kolejna doba misji</button>
4</template>
5
6<script setup>
7import { ref } from 'vue'
8
9const daysRemaining = ref(180)
10
11function advanceDay() {
12 daysRemaining.value = Math.max(0, daysRemaining.value - 1)
13}
14</script>Kliknij przycisk, a licznik spadnie o jeden i będzie spadał aż do zera, gdzie się zatrzyma. Najważniejsze jest jednak to, czego w tym kodzie nie ma. Nie ma ani jednej linii, która wyszukiwałaby akapit w dokumencie i podmieniała w nim tekst. Nie ma odświeżania panelu. Funkcja
advanceDay zajmuje się wyłącznie danymi, a przerysowanie ekranu jest zadaniem Vue, które samo zauważyło zmianę w kontenerze. Szablon również się nie zmienił - {{ daysRemaining }} wygląda dokładnie tak samo jak wtedy, gdy liczba stała w miejscu.Do przycisku prowadzi w Vue jedna droga i warto wiedzieć, dlaczego pozostałe są ślepe.
Zwykły atrybut HTML
onclick="advanceDay()" wygląda znajomo i technicznie jest poprawnym HTML-em - tyle że obsługuje go przeglądarka, a nie Vue. Przeglądarka poszuka funkcji advanceDay wśród funkcji globalnych, a Twoja funkcja żyje wewnątrz komponentu i na zewnątrz w ogóle nie istnieje. Efekt jest taki, że kliknięcie nic nie robi, a w konsoli ląduje błąd o nieznanej nazwie.Zapis
v-event:click="advanceDay" brzmi sensownie, ale dyrektywy v-event w Vue po prostu nie ma. Lista dyrektyw jest zamknięta i zdarzeniami zajmuje się w niej wyłącznie v-on.Zapis
bind:click="advanceDay" myli dwa różne narzędzia. W Vue istnieje v-bind ze skrótem w postaci dwukropka, ale służy on do wiązania atrybutów i właściwości, nigdy do zdarzeń. Zapamiętaj ten podział, bo wraca w każdym szablonie: małpa to zdarzenia, dwukropek to atrybuty.Nie każde kliknięcie zasługuje na własną funkcję. Gdy cała reakcja mieści się w jednej operacji, możesz wpisać ją wprost do szablonu, bo
@click przyjmuje nie tylko nazwę funkcji, ale też wyrażenie JavaScript. Obowiązuje w nim ta sama zasada co w wąsach, więc .value znika: piszesz nazwę zmiennej tak, jakby była zwykłą liczbą.Zobacz to na drugim panelu stacji - liczniku energii, w którym dwa przyciski zmieniają poziom o pięć punktów, a trzeci przywraca stan wyjściowy.
1<template>
2 <p>Energia: {{ energyLevel }}%</p>
3
4 <button @click="energyLevel -= 5">- 5</button>
5 <button @click="energyLevel += 5">+ 5</button>
6 <button @click="resetEnergy">Reset</button>
7</template>
8
9<script setup>
10import { ref } from 'vue'
11
12const energyLevel = ref(50)
13
14function resetEnergy() {
15 energyLevel.value = 50
16}
17</script>Wszystkie trzy przyciski działają na ten sam kontener
energyLevel i wszystkie trzy odświeżają ten sam akapit - to się nie zmienia niezależnie od zapisu. Różnica jest wyłącznie w tym, gdzie mieszka instrukcja: w dwóch pierwszych przyciskach w szablonie, w trzecim w funkcji.Kiedy wybrać którą formę? Moja rekomendacja, @name, jest prosta: wyrażenie w szablonie tylko wtedy, gdy to jedna krótka operacja, którą da się przeczytać jednym rzutem oka. Wszystko, co ma warunek, dwa kroki albo własną nazwę wartą zapamiętania, przenieś do funkcji i podepnij ją pod
@click po nazwie. Szablon ma pokazywać, co się dzieje, a nie chować logikę misji między znacznikami.Zostało ostatnie narzędzie z dzisiejszej czwórki. Panel pokazuje status stacji napisem, ale w Centrum Kontroli liczy się kolor: zielony znaczy spokój, żółty znaczy sprawdź. Zwykły atrybut
class="ok" jest wpisany na sztywno dokładnie tak samo jak wcześniej liczba 180 - i tak samo nie potrafi się zmienić.Do atrybutów Vue ma dyrektywę
v-bind, a jej skrótem jest dwukropek postawiony przed nazwą atrybutu. Zapis :class mówi Vue: to, co stoi po prawej stronie, nie jest napisem, tylko wyrażeniem JavaScript - policz je, a wynik wstaw jako klasę elementu. Najprostsza wersja to nazwa zmiennej, w której trzymasz nazwę klasy.1<template>
2 <span :class="statusClass">{{ statusLabel }}</span>
3</template>
4
5<script setup>
6import { ref } from 'vue'
7
8const statusClass = ref('ok')
9const statusLabel = ref('WSZYSTKIE OK')
10</script>To działa, ale ma słaby punkt: dwa kontenery opisują jeden stan i nic nie pilnuje, żeby były zgodne. Wystarczy, że gdzieś zmienisz klasę, a zapomnisz o napisie, i panel pokaże zielony alert. Lepiej trzymać jedną prawdę - zwykłą wartość logiczną - i wyprowadzać z niej i klasę, i napis. Wystarczy do tego operator warunkowy, czyli znany z JavaScriptu zapis: warunek, znak zapytania, wartość gdy prawda, dwukropek, wartość gdy fałsz.
1<template>
2 <span :class="systemsOk ? 'ok' : 'alert'">
3 {{ systemsOk ? 'WSZYSTKIE OK' : 'ALERT' }}
4 </span>
5</template>
6
7<script setup>
8import { ref } from 'vue'
9
10const systemsOk = ref(true)
11</script>Jeden kontener, dwa miejsca, które z niego korzystają, i komplet zawsze spójny. Zwróć uwagę, że to samo wyrażenie warunkowe raz stoi po dwukropku w atrybucie, a raz wewnątrz wąsów - to ta sama składnia JavaScriptu użyta w dwóch różnych miejscach szablonu. Nie zmieniło się też nic w samym znaczniku
<span>: to wciąż zwykły element, tyle że o jego klasie decyduje teraz stan misji.Czego
:class nie robi? Nie tworzy styli. Podaje wyłącznie nazwę klasy, a wygląd nadal opisuje zwykły CSS - u nas w sekcji <style scoped>, dokładnie takiej samej jak w poprzedniej lekcji.1<style scoped>
2.ok { color: #00ff88; }
3.alert { color: #ffd60a; }
4</style>Dwie reguły, żadnej magii - i to jest cała tajemnica dynamicznych klas. Dyrektywa przełącza nazwę, CSS zamienia nazwę na kolor. Gdy zmienna
systemsOk przyjmie wartość fałsz, Vue zdejmie ze znacznika klasę ok, założy alert, a przeglądarka przemaluje napis na żółto. Same reguły w arkuszu pozostają nietknięte.Na koniec zapowiedź:
:class przyjmuje też zapis obiektowy, na przykład :class="{ alert: !systemsOk }", który dokłada klasę tylko wtedy, gdy warunek jest prawdziwy. Wrócimy do niego przy dyrektywach, gdy panel będzie miał więcej niż dwa stany.Trzy pomyłki wracają w tym miejscu regularnie.
Zapis
class="{{ statusClass }}" to próba użycia wąsów w atrybucie. Interpolacja obsługuje wyłącznie treść tekstową elementu, więc Vue nie policzy tu wyrażenia i nie podstawi wartości zmiennej. Klasa ok nigdy nie trafi na element, a kolor się nie pojawi.Zapis
css-class="statusClass" nie istnieje ani w HTML-u, ani w Vue. Przeglądarka potraktuje to jako nieznany atrybut i element zostanie bez żadnej klasy.Zapis
style:class="statusClass" miesza dwie rzeczy. Vue ma wprawdzie :style, ale ono ustawia style pisane wprost na elemencie, a nie nazwy klas - i nie ma czegoś takiego jak style:class. Klasa zawsze idzie przez :class.Masz już wszystkie cztery narzędzia, więc czas złożyć z nich panel statusu misji - taki, jaki naprawdę wisi w Centrum Kontroli. Pokazuje nazwę misji, bieżącą fazę, dystans do Marsa, dni do celu i status stacji, a przycisk przesuwa misję do kolejnej fazy.
Dojdzie jedna nowość, ale spoza świata Vue: zwykła tablica JavaScript z nazwami faz oraz kontener trzymający indeks fazy bieżącej. Zwróć uwagę, że tablica
phases nie jest zawinięta w ref - jej zawartość nigdy się nie zmienia, więc nie ma czego obserwować. Reaktywność kosztuje, a w kosmosie nie wozi się zbędnego ładunku: kontenery zakładaj tylko na wartości, które faktycznie się zmieniają i mają odświeżać ekran. Operator reszty z dzielenia % sprawia, że po ostatniej fazie licznik wraca na początek - to wygoda demonstracji, żeby dało się klikać w kółko, a nie opis prawdziwej misji.1<template>
2 <div class="mission-status">
3 <header>
4 <h1>{{ missionName }}</h1>
5 <span class="phase">{{ currentPhase }}</span>
6 </header>
7
8 <div class="metrics">
9 <div class="metric">
10 <label>Dystans do Marsa</label>
11 <span>{{ distanceToMars }} mln km</span>
12 </div>
13 <div class="metric">
14 <label>Czas do celu</label>
15 <span>{{ daysRemaining }} dni</span>
16 </div>
17 <div class="metric">
18 <label>Status stacji</label>
19 <span :class="systemsOk ? 'ok' : 'alert'">
20 {{ systemsOk ? 'WSZYSTKIE OK' : 'ALERT' }}
21 </span>
22 </div>
23 </div>
24
25 <button @click="nextPhase">Kolejna faza misji</button>
26 </div>
27</template>
28
29<script setup>
30import { ref } from 'vue'
31
32const phases = ['Przygotowanie', 'Start', 'Lot', 'Orbita Marsa', 'Baza Nova']
33
34const missionName = ref('MARS ONE - Pierwsza Kolonia')
35const currentPhase = ref(phases[0])
36const phaseIndex = ref(0)
37const distanceToMars = ref(225)
38const daysRemaining = ref(180)
39const systemsOk = ref(true)
40
41function nextPhase() {
42 phaseIndex.value = (phaseIndex.value + 1) % phases.length
43 currentPhase.value = phases[phaseIndex.value]
44
45 distanceToMars.value = Math.max(0, distanceToMars.value - 45)
46 daysRemaining.value = Math.max(0, daysRemaining.value - 36)
47}
48</script>Kliknij przycisk kilka razy: etykieta fazy przesuwa się przez kolejne etapy, dystans i licznik dni maleją, a oba zatrzymują się na zerze. Wszystko to bez jednej linii kodu, która dotykałaby dokumentu. Popatrz jeszcze raz na szablon - poza wąsami, jednym dwukropkiem i jedną małpą to zwykły HTML, ten sam, który pisałeś od pierwszego świata. To się nie zmieniło i nie zmieni: Vue nie zastępuje HTML-a, tylko podpina go do danych.
Nie zmieniła się też liczba miejsc, w których opisujesz stan misji. Jest dokładnie jedno: sekcja
<script setup>. Szablon ten stan wyłącznie pokazuje, funkcja wyłącznie go modyfikuje, a Vue pilnuje, żeby ekran zawsze zgadzał się z danymi. Ten podział ról będzie Ci towarzyszył przez cały świat NOVA LAB - w kolejnej lekcji zobaczysz, jak takie moduły spina się w jedną aplikację misji.Zapamiętaj, @name:
ref() tworzy kontener, wąsy go pokazują, małpa reaguje na astronautę, a dwukropek maluje panel według stanu - i to jest komplet narzędzi pierwszego działającego systemu.