Field watcher in Vue.js

PROBLEM

Vue.js allows you to interact with the DOM through interpolation, data binding or v-bind. The above-mentioned methodologies work, whenever they work, we use the value of a given field and we do not need to complete an addition to the work. What if we all need to process data before we use it? Or are we interested in the field values themselves because we want to have the changes as a function call? We can't add logic to the object's data, after all.

Suppose we are setting the effective dates of a certain campaign. We want the campaign end date to be based on the user's entered start date. This restriction says:

  • if the user has entered the start date, then the end date should not be less than the start date
  • if the user did not enter the start date, then the end date should be today (we do not want to set up the campaign in the past)

Below we have the template of the Vue component created with Vuetify, which contains two windows for selecting the dates: start and end.

<!--start date-->
<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>
<!--start date-->

<!--end date-->
<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>
<!--end date-->

And the clip below shows the JavaScript code that supports it. At this point, it only contains a date object, with a campaign object with dates in it.

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

THREE POSSIBLE APPROACHES

To some extent, the answer to this need would be to call the method from the methods object. However, Vue.js has functionalities dedicated to this type of task: computed properties and watch properties. Both allow you to react to a change in the value of a field, but their behaviour is slightly different.

The computed object, like the data object, is used to store field values. The fields stored in it can be used in the same way as the fields from the data object, e.g. in interpolation. However, instead of specific values, it stores a data processing function. This function contains the logic for calculating the field value and must always return a value. The result of calling it is cached, so the function is run only when the field value is changed.

The advantage of watch property over computed property is an asynchronous operation. This allows us to wait for a response from the server or call another asynchronous function. In addition, the watch property allows you to call the code without the need to return a value, as is the case with calculated property. The watch object takes the name of the data object field as the key.

So let's check how they work in practice.

1. METHOD CALL

We need a field in the data object that represents the minimum end date. So we declare minEndDate initially as an empty String. We also need to add a method that implements the condition logic. We add the methods object and the calculateMinEndDate method to it.

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 of the v-date-picker component that handles the end date expects a date. So we assign to it the value of the field :min="minEndDate". Finally, we need to ensure that the method runs whenever the start date value changes. We add event listener @change="calculateMinEndDate" to the field with the start date selection.

<!--start date-->
<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>
<!--start date-->

<!--end date-->
<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>
<!--end date-->

Phew… it cost us quite a lot of work. Now let's see what the solution using computed and watch properties looks like.

2. COMPUTED PROPERTY

We create the computed object. Since the computed property stores functions, we can get rid of the minEndDate field in the data object and give the same name to the field in the computed object. The body of the calculateMinEndDate method can be moved to the minEndDate field and the method itself removed from the methods object. Computed property expects the function to return a value, so instead of assigning dates to this.minEndDate, we just return them.

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

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

We must remember to remove the event listener @change="calculateMinEndDate" added to the field with the start date selection. The computed property mechanism automatically tracks changes, so the even listener will no longer be needed. Assigning minEndDate to property min remains the same because we moved the field name from the data object to computed, and we can use computed property objects in the same way as data.

All we needed was to create a minEndDate field in the computed object and assign it to the property.

3. WATCH PROPERTY

We restore the minEndDate field in the data object. We change the computed object to watch. We also need to edit it additionally. We provide campaign.dateFrom as the key, because minEndDate is to depend on this field. We can take advantage of assigning the new field value to the function argument and use it instead of directly referring to this.campaign.dateFrom. We set a timeout in the function body to illustrate the possibility of calling asynchronous code. However, it is completely unnecessary for the correct operation of the example.

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);
  }
}

When configuring the solution with watch property, we needed little more lines of code than for computed property. At the same time, we gained the ability to run asynchronous code in response to a change.

SUMMARY

Both the computed and watch properties functionalities allow you to react to the change of the field value. Both require significantly less work than simply running the function in response to a change. In a large number of cases, they allow you to get the same result. So if you need to run asynchronous code use watch property. In other cases, use computed property rather, because caching makes this functionality better optimized and allows Vue.js to run more efficiently.