Zagadnienie

Vue.js umożliwia interakcję z DOM poprzez interpolację, data binding czy dyrektywę v-bind. Wymienione metody sprawdzają się, gdy jedynie korzystamy z wartości danego pola i nie potrzebujemy jej dodatkowo obrabiać przed wykorzystaniem. Co jednak jeśli potrzebujemy przetworzyć dane zanim z nich skorzystamy? Albo interesuje nas sam fakt zmiany wartości pola, ponieważ chcemy go użyć jako triggera do wywołania funkcji? Nie możemy przecież dołożyć logiki do obiektu data.

Załóżmy, że ustawiamy daty obowiązywania pewnej kampanii. Chcemy, aby data końcowa kampanii była uzależniona od wprowadzonej przez użytkownika daty początkowej. Ograniczenie to mówi:

  • jeśli użytkownik wprowadził datę początkową, to data końcowa powinna być nie mniejsza niż data początkowa,
  • jeśli użytkownik nie podał daty początkowej, to data końcowa powinna być dniem dzisiejszym (nie chcemy ustawiać kampanii w przeszłości).

Poniżej mamy przedstawiony template komponentu Vue stworzony przy pomocy Vuetify zawierający dwa okna wyboru dat: początkowej i końcowej.

<!--data początkowa-->
<v-menu transition="scale-transition" offset-y min-width="290px">
    <template v-slot:activator="{ on }">
        <v-text-field v-model="campaign.dateFrom" v-on="on"> </v-text-field>
    </template>
    <v-date-picker v-model="campaign.dateFrom" no-title scrollable locale="pl"></v-date-picker>
</v-menu>
<!--data początkowa-->

<!--data końcowa-->
<v-menu transition="scale-transition" offset-y min-width="290px">
    <template v-slot:activator="{ on }">
        <v-text-field v-model="campaign.dateTo" v-on="on"></v-text-field>
    </template>
    <v-date-picker v-model="campaign.dateTo" no-title scrollable locale="pl"></v-date-picker>
</v-menu>
<!--data końcowa-->

Zaś poniższy wycinek przedstawia obsługujący go kod JavaScript. Zawiera w tym momencie jedynie obiekt data, a w nim obiekt campaign z datami.

export default {

    data: () => ({        
        campaign: {
            dateFrom: '',
            dateTo: '',
            },
    }),
}

Trzy możliwe podejścia

W pewnym stopniu odpowiedzią na taka potrzebę byłoby wywołanie metody z obiektu methods. Jednak Vue.js posiada dedykowane do tego typu zadań funkcjonalności: computed properties i watch properties. Obie pozwalają na reagowanie na zmianę wartości pola, jednak ich sposób działania nieco się różni.

Obiekt computed podobnie jak obiekt data służy do przechowywania wartości pól. Pola w nim przechowywane możemy używać w ten sam sposób jak pola z obiektu data np. w interpolacji. Jednak zamiast konkretnych wartości przechowuje funkcję przetwarzającą dane. Funkcja ta zawiera logikę przeliczenia wartości pola i zawsze musi zwracać wartość. Rezultat jej wywołania jest cachowany, przez co funkcja jest uruchamiana tylko w momencie zmiany wartości pola.

Przewagą watch property nad computed property jest działanie asynchroniczne. Dzięki temu możemy poczekać na odpowiedź z serwera lub wywołać inną asynchroniczną funkcję. Dodatkowo watch property pozwala na wywołanie kodu bez konieczności zwracania wartości, jak to ma miejsce w przypadku calculated property. Obiekt watch jako klucz przyjmuje nazwę pola obiektu data.

Sprawdźmy zatem jak działają w praktyce.

1. Wywołanie metody

Potrzebujemy pola w obiekcie data reprezentującego minimalną datę końcową. Deklarujemy więc minEndDate początkowo jako pusty String. Musimy również dodać metodę implementującą logikę warunków. Dodajemy obiekt methods, a w nim metodę calculateMinEndDate.

data: () => ({        
    campaign: {
        dateFrom: '',
        dateTo: '',
        },
    minEndDate: ''
}),

methods: {
    calculateMinEndDate() {
        if(this.campaign.dateFrom !== '') {
            this.minEndDate = this.campaign.dateFrom
        } else {
            this.minEndDate = new Date().toISOString().substring(0,10)
        }
    }
}

Property min komponentu v-date-picker obsługującego datę końcową oczekuje podania daty. Przypisujemy więc do niej wartość pola :min="minEndDate". Na koniec musimy zapewnić uruchamianie się metody przy każdej zmianie wartości daty początkowej. Do pola z wyborem daty początkowej dodajemy event listener @change="calculateMinEndDate".

<!--data początkowa-->
<v-menu transition="scale-transition" offset-y min-width="290px">
    <template v-slot:activator="{ on }">
        <v-text-field v-model="campaign.dateFrom" v-on="on"> </v-text-field>
    </template>
    <v-date-picker v-model="campaign.dateFrom" no-title scrollable locale="pl" @change="calculateMinEndDate"></v-date-picker>
</v-menu>
<!--data początkowa-->

<!--data końcowa-->
<v-menu transition="scale-transition" offset-y min-width="290px">
    <template v-slot:activator="{ on }">
        <v-text-field v-model="campaign.dateTo" v-on="on"></v-text-field>
    </template>
    <v-date-picker v-model="campaign.dateTo" no-title scrollable locale="pl" :min="minEndDate"></v-date-picker>
</v-menu>
<!--data końcowa-->

Uff… kosztowało nas to całkiem sporo pracy. Teraz zobaczmy jak wygląda rozwiązanie z użyciem computed i watch properties.

2. Computed property

Tworzymy obiekt computed. Dzięki temu, że computed property przechowuje funkcje, możemy pozbyć się pola minEndDate w obiekcie data, a taką samą nazwę nadać polu w obiekcie computed. Ciało metody calculateMinEndDate możemy przenieść do pola minEndDate, a samą metodę usunąć z obiektu methods. Computed property oczekuje, że funkcja będzie zwracała wartość, zatem zamiast przypisania dat do this.minEndDate po prostu je zwracamy.

data: () => ({        
    campaign: {
        dateFrom: '',
        dateTo: '',
        },
}),

computed: {
    minEndDate() {
        if(this.campaign.dateFrom !== '') {
            return this.campaign.dateFrom
        } else {
            return new Date().toISOString().substring(0,10)
        }
    }
}

Musimy pamiętać o usunięciu dodanego do pola z wyborem daty początkowej event listenera @change="calculateMinEndDate". Mechanizm computed property automatycznie śledzi zmiany, zatem even listener nie będzie już potrzebny. Przypisanie minEndDate do propert min pozostaje niezmienne, ponieważ przenieśliśmy nazwę pola z obiektu data do computed, a obiekty z computed property możemy wykorzystywać w ten sam sposób co data.

Wszystko czego potrzebowaliśmy to utworzenie pola minEndDate w obiekcie computed i przypisanie go do property

3. Watch property

Przywracamy pole minEndDate w obiekcie data. Zmieniamy obiekt computed na watch. Musimy go też dodatkowo edytować. Jako klucz podajemy campaign.dateFrom, gdyż to od tego pola ma zależeć minEndDate. Możemy skorzystać z przypisania nowej wartości pola do argumentu funkcji i użyć go zamiast bezpośredniego odwołania do this.campaign.dateFrom. Ustawiamy timeout w ciele funkcji, aby zobrazować możliwość wywołania kodu asynchronicznego. Jednak jest on całkowicie zbędny dla poprawnego działania przykładu.

data: () => ({        
    campaign: {
        dateFrom: '',
        dateTo: '',
        },
    minEndDate: ''
}),

watch: {
    'campaign.dateFrom': function(value) {
        setTimeout(() => {
            if(value !== '') {
                this.minEndDate = value
            } else {
                this.minEndDate = new Date().toISOString().substring(0,10)
            }
        }, 3000)

    }
}

Konfigurując rozwiązanie z watch property potrzebowaliśmy niewiele więcej linijek kodu niż przy computed property. Jednocześnie zyskaliśmy możliwość uruchamiania kodu asynchronicznego w odpowiedzi na zmianę.

Podsumowanie

Obie funkcjonalności computed i watch properties pozwalają na reakcje na zmianę wartości pola. Obie wymagają znacznie mniejszego nakładu pracy, niż w przypadku zwykłego uruchomienia funkcji w reakcji na zmianę. W dużej ilości przypadków pozwalają uzyskać ten sam rezultat. Jeśli więc potrzebujesz uruchomić kod asynchroniczny wykorzystaj watch property. W pozostałych przypadkach używaj raczej computed property, ponieważ dzięki cachowaniu ta funkcjonalność jest lepiej zoptymalizowana i pozwala Vue.js działać sprawniej.