Methods, Computed Properties, and Watchers

NOTE: the exercises from this section are here (methods & computed properties) and here (watchers)

Methods

  • bound to the Vue instance --- useful for functions you would like to access in directives, other methods, etc
  • when we use @click we will typically make a corresponding method
  • example: Simple Event Handler Method Example
    • when we say this inside a method we are always referring to data
    • automatically have access to the event e in the method without having to pass it in the @mousemove (shortcut for v-on)
    • we are using the v-bind shortcut : to bind the style and make the changing background color as we move the mouse (test it out here)
  • example --- this will be an ongoing example that we will keep refining:
    const App = {
      data() {
        return {
          newComment: '',
          comments: [
            'Looks great Julianne!',
            'I love the sea',
            'Where are you at?'
          ]
        }
      },
      methods: {
        addComment() {
          this.comments.push(this.newComment)
          this.newComment = ''
        }
      }
    }
    
    Vue.createApp(App).mount('#app')
    <div id="app">
      <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/vue-post-photo.jpg" class="main-photo">
      <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/vue-main-profile.jpg" class="main-profile">
      <div class="main-info">
        <span class="name">Julianne Delfina</span> 
        <h3>"It's lovely after it rains"</h3>
      </div>
      <hr>
    
      <ul>
        <li v-for="comment in comments" :key="comment">
          {{ comment }}
        </li>
      </ul>
    
      <input
        @keyup.enter="addComment"
        v-model="newComment"
        placeholder="Add a comment"
      />
    </div>    
    body {
    font-family: 'Playfair Display', serif;
    }
    
    #app {
    background: #212222;
    color: #fff;
    letter-spacing: 0.04em;
    text-align: center;
    margin: 60px;
    width: 370px;
    margin: 0 auto;
    display: table;
    padding: 20px;
    line-height: 1.4em;
    }
    
    .name {
    color: #ccc;
    }
    
    small {
    color: #bbb;
    font-size: 10px;
    }
    
    h3 {
    margin: 5px 0 4px;
    }
    
    .main-photo {
    width: 300px;
    }
    
    .main-profile {
    float: left;
    border: 3px solid white;
    margin: -25px 0 0 20px;
    position: relative;
    width: 80px;
    }
    
    .main-info {
    float: left;
    padding: 10px 20px;
    text-align: left;
    margin-bottom: 15px;
    &:after {
    content: "";
    display: table;
    clear: both;
    }
    }
    
    li {
    list-style: none outside none;
    text-align: left;
    padding: 10px 0;
    border-bottom: 1px solid #555;
    }
    
    ul {
    margin: 0;
    padding: 0 35px;
    }
    
    hr {
    margin: 75px 0 0 32px;
    width: 300px;
    border-top: 0;
    border-bottom: 1px solid #555;
    }
    
    input {
    font-family: 'Playfair Display', serif;
    width: 280px;
    margin: 30px 0;
    padding: 8px 10px;
    outline: 0;
    }
    Output

Methods in Forms

<div id="app">
  <form @submit.prevent="submitForm">
    <div>
      <label for="name">Name:</label><br>
      <input id="name" type="text" v-model="name" required/>
    </div>
    <div>
      <label for="email">Email:</label><br>
      <input id="email" type="email" v-model="email" required/>
    </div>
    <div>
      <label for="caps">HOW DO I TURN OFF CAPS LOCK:</label><br>
      <textarea id="caps" v-model="caps" required></textarea>
    </div>
    <button :class="[name ? activeClass : '']" type="submit">Submit</button>
    <div>
      <h3>Response from server:</h3>
      <pre>{{ response }}</pre>
    </div>
  </form>
</div>
  • the .prevent stops the page from reloading when the form is submitted
  • should create labels for inputs --- it allows screen readers to read it out
const App = {
  data() {
    return {
      name: '',
      email: '',
      caps: '',
      response: '',
      activeClass: 'active'
    }
  },
  methods: {
    submitForm() {
      axios.post('//jsonplaceholder.typicode.com/posts', {
        name: this.name,
        email: this.email,
        caps: this.caps
      }).then(response => {
        this.response = JSON.stringify(response, null, 2)
      }).catch(error => {
        this.response = 'Error: ' + error.response.status
      })
    }
  }
}

Vue.createApp(App).mount('#app')

Form

Sorting Table Data with v-for

<div id="app">
  <h3>Sort titles by: 
    <button @click="sortLowest">Lowest Rated</button>
    <button @click="sortHighest">Highest Rated</button>
  </h3>
  <table>
    <thead>
      <tr>
        <th v-for="key in columns">
          {{ key }}
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="entry in ratingsInfo">
        <td v-for="key in columns">
          {{entry[key]}}
        </td>
      </tr>
    <tbody>
  </table>
</div>
const App = {
  data() {
    return {
      columns: ["title", "rating"],
      ratingsInfo: [
        { title: `White Chicks`, rating: 82 },
        { title: `Grey's Anatomy`, rating: 98 },
        { title: `Prison Break`, rating: 98 },
        { title: `How I Met Your Mother`, rating: 94 },
        { title: `Supernatural`, rating: 95 },
        { title: `Breaking Bad`, rating: 97 },
        { title: `The Vampire Diaries`, rating: 91 },
        { title: `The Walking Dead`, rating: 98 },
        { title: `Pretty Little Liars`, rating: 96 },
        { title: `Once Upon a Time`, rating: 98 },
        { title: `Sherlock`, rating: 95 },
        { title: `Death Note`, rating: 77 },
        { title: `Naruto`, rating: 88 },
        { title: `Arrow`, rating: 96 },
        { title: `Black Mirror`, rating: 80 },
        { title: `The Originals`, rating: 74 },
        { title: `The 100`, rating: 97 },
        { title: `Masha and the Bear`, rating: 81 },
        { title: `Hunter X Hunter`, rating: 57 },
        { title: `Marvel's Luke Cage`, rating: 95 },
        { title: `Marvel's Iron Fist`, rating: 98 }
      ]
    }
  },
  methods: {
    sortLowest() {
      this.ratingsInfo.sort((a, b) => a.rating > b.rating ? 1 : -1);
    },
    sortHighest() {
      this.ratingsInfo.sort((a, b) => a.rating < b.rating ? 1 : -1);
    }
  }
}

Vue.createApp(App).mount('#app')
  • data is hardcoded here, but actually comes from a real netflix api
  • can't use arrow functions in top level of methods because lose the this. binding to the data and we need that relationship

Sort lowest Sort highest

Computed Properties

  • calculations that will be cached (definition) and will only update when needed
  • highly performant, but needs to be used with understanding
  • Simplest example (not a real use case):
<div id="app">
  <h3>Your Name: <input v-model.lazy="userData" /></h3>
  <h2 v-if="userData">Initial entry: {{ userData }}</h2>
  <h2 v-if="userData">Computed Value: {{ greeting }}</h2>
</div>
const App = {
  data() {
    return {
      userData: ''
    }
  },
  computed: {
    greeting() {
      return `You're a monster, ${this.userData}!`
    }
  }
}

Vue.createApp(App).mount('#app')
  • computed property is giving us a new view of that data --- not mutating userData, but able to use userData and return a different value to it that's slightly different from what the initial was --- greeting will only be evaluated if this.userData changes
  • greeting looks like a method, but it is used how we use data --- it is inserted into the html template

simplest computed example

Computed Methods
* runs only when a dependency has changed * runs whenever an update occurs
* cached * not cached
* should be used as a property, in place of data * typically invoked from v-on/@/etc, but flexible
* by default getter only, but you can define a setter * getter/setter
  • search added to movie sorting example from above:
...
<tbody>
<tr v-for="entry in filteredFilms">
  <td v-for="key in columns">
    {{entry[key]}}
  </td>
</tr>
<tbody>
...
...
filterText: ''    // in data
...
computed: {
  filteredFilms() {
    let filter = new RegExp(this.filterText, 'i')
    return this.ratingsInfo.filter(el => el.title.match(filter))
  }
}
...
  • use regex to match --- not case sensitive
  • filters based on how this.filterText compares to the titles in this.ratingsInfo
  • quickly updates as computed changes --- good for search implementations, etc

first letter entered in search second letter entered in search

  • uses computed to compute a counter
<div id="app">
  <p>counter: {{ counter }}</p>
  <p>counter computed: {{ countupComp }}</p>
  <button @click="countup">Increase</button>
</div>
const App = {
  data() {
    return {
      counter: 0
    }
  }, 
  methods: {
    countup() {
      this.counter++;
    }
  },
  computed: {
    // another view on the same data
    countupComp() {
      return this.counter + 1;
    }
  }
}

Vue.createApp(App).mount('#app')
  • evaluates only when this.counter changes

Differences in Vue2 & 3

  • surface API is the same
  • Vue 2 filters were deprecated because everything can be done with methods and computed properties
  • in Vue2 everything was in one place --- in Vue3 everything exists in different packages, which lets us exclude parts we don't need to make a smaller build

Vue's Reactivity System & Watchers

Reactivity in Vue3

How Does Vue3 Do This?

  • detect when there's a change in one of the values proxies will do this fpr us
  • track the function that changes it --- do this using track
  • trigger the function so it can update the final value --- do this using trigger
Proxies
  • NOTE: new in ES6 --- previously Object.defineProperty
  • a proxy is an object that encases another object and allows you to intercept it --- new Proxy(target, handler)
  • basic example:
    const dinner = {
      meal: 'tacos'
    }
    
    const handler = {
      get(target, prop) {
        return target[prop]    
      } 
    }   
    
    const proxy = new Proxy(dinner, handler)
    console.log(proxy.meal)     
    
    // tacos
    • can do other things before return:
      const handler = {
        get(target, prop) {
          console.log('intercepted!')
          return target[prop]    
        } 
      }   
      
      const proxy = new Proxy(dinner, handler)
      console.log(proxy.meal)     
      
      // intercepted!
      // tacos
    • the target[prop] is not automatically returned, we have to make sure to do it
    • if we don't return it:
      const handler = {
        get(target, prop) {
          console.log('we swapped out your dinner!')
          return 'burger'
        } 
      }   
      
      const proxy = new Proxy(dinner, handler)
      console.log(proxy.meal)     
      
      // we swapped out your dinner!
      // burger
      • this ability in JS is called a TRAP!
  • using Reflect:
    const dinner = {
      meal: 'tacos'
    }
    
    const handler = {
      get(target, prop, receiver) {
        return Reflect.get(...arguments)    
      } 
    }   
    
    const proxy = new Proxy(dinner, handler)
    console.log(proxy.meal)     
    
    // tacos
    • end up with the same thing...so why would we do it?
      • *** Reflect binds this properly ***
  • using track:
    const dinner = {
      meal: 'tacos'
    }
    
    const handler = {
      get(target, prop, receiver) {
        track(target, prop)
        return Reflect.get(...arguments)    
      } 
    }   
    
    const proxy = new Proxy(dinner, handler)
    console.log(proxy.meal)     
    
    // tacos
    • return the same thing, but also track the things that are changing --- want to make sure have a function that keeps track of that information --- we will know what is changing about it
    • *** track (in Vue) saves any changes ***
  • using set and trigger:
    const dinner = {
      meal: 'tacos'
    }
    
    const handler = {
      get(target, prop, receiver) {
        track(target, prop)
        return Reflect.get(...arguments)    
      },
      set(target, key, value, receiver) {
        trigger(target, key)
        return Reflect.set(...arguments)
      } 
    }   
    
    const proxy = new Proxy(dinner, handler)
    console.log(proxy.meal) 
    
    // tacos    
    • *** trigger (in Vue) runs the changes ***
  • you don't want to do anything when the tracked value stays the same:
    const dinner = {
      meal: 'tacos'
    }
    
    const handler = {
      get(target, prop, receiver) {
        track(target, prop)
        return Reflect.get(...arguments)    
      },
      set(target, key, value, receiver) {
        let oldValue = target[key]
        let result = Reflect.set(...arguments)
        if (oldValue != result) {
          trigger(target, key)
        }
        return result
      } 
    }   
More Base JS Concepts
  • Set()
    • a set is a series of only values (similar to an array), where any particular value can only be inserted once
    • example:
    const myLunchItems = new Set(['🌮', '🍔', '🌮'])
    console.log(myLunchItems)
    
    // set(2) {"🌮", "🍔"}
  • Map()
    • a map is a series of keys and values, similar to an object, but with some differences:
      • key/value pairs remember their explicit ordering
      • performs better in scenarios involving frequent additions and removals
      • like Set(), you can only add key/value pairs once
      • it has some nice methods like: size, has, set, clear, delete(key)
      • example:
        const newMap = new Map()
        newMap.set('lunch1', '🌮')
        // Map(1) {"lunch1" => "🌮"}
        newMap.set('lunch2', '🍔')
        // Map(2) {"lunch1" => "🌮", "lunch2" => "🍔"}
        newMap.set('lunch3', '🌮')
        // Map(2) {"lunch1" => "🌮", "lunch2" => "🍔"}
    • Sarah's Video Example (if needed...password: !vue!)
  • WeakMap()
    • similar to Map(), but the references are held weakly --- meaning, if you delete something the reference can be garbage collected (in a Map() it can't)
    • this also means it loses the explicit ordering (Sarah's slide says 'implicit', but I think it should have said 'explicit' because that's what Map() has that it's losing)
    • in Vue3 we want these to be garbage collected

Watchers

  • good for asynchronous updates and updates/transitions with data changes
  • can 'watch' any data property declared on the Vue instance --- they will have the same name
    const App = {
      data() {
        return {
          counter: 0
        }
      },
      watch: {
        counter() {
          console.log('The counter has changed!')
        }
      }
    }
        
    Vue.createApp(App).mount('#app')
  • we have access to the old value and the new value
    watch: {
      watchedProperty(newValue, oldValue) {
        // your code
      }
    }
  • can gain access to nested values with 'deep':
    watch: {
      watchedProperty(newValue, oldValue) {
        deep:true,
        nestedWatchedproperty(newValue, oldValue) {  
          // your code
        }
      }
    }
  • example:
    const App = {
        data() {
            return {
                counter: 0
            }
        },
        watch: {
            counter(newValue, oldValue) {
                console.log(`The counter has changed! It was ${oldValue}, it's now ${newValue}`)
            }
        }
    }
    
    Vue.createApp(App).mount('#app')
    
    // The counter has changed! It was 0, it's now 1       pen.js:10 
    // The counter has changed! It was 1, it's now 2       pen.js:10 
    // The counter has changed! It was 2, it's now 1       pen.js:10 
    // The counter has changed! It was 1, it's now 0       pen.js:10 
    // The counter has changed! It was 0, it's now 1       pen.js:10 
    // The counter has changed! It was 1, it's now 2       pen.js:10 
    // The counter has changed! It was 2, it's now 3       pen.js:10 
    // The counter has changed! It was 3, it's now 4       pen.js:10 
    // The counter has changed! It was 4, it's now 5       pen.js:10

Copyright © 2022