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:
- when we say
this
inside a method we are always referring todata
- automatically have access to the event
e
in the method without having to pass it in the@mousemove
(shortcut forv-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)
- when we say
- 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; }
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')
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
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 useuserData
and return a different value to it that's slightly different from what the initial was ---greeting
will only be evaluated ifthis.userData
changes greeting
looks like a method, but it is used how we use data --- it is inserted into the html template
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 inthis.ratingsInfo
- quickly updates as computed changes --- good for search implementations, etc
- 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
- reactive programming is programming with asynchronous data streams
- a stream is a sequence of ongoing events ordered in time that offer some hooks with which to observe it
- when we use reactive premises for building applications, this means it's very easy to update state in reaction to events
- Recommended Reading: The Introduction to Reactive Programming you've been missing
- Sarah's Video Example (if needed...password: !vue!)
- Vue3 Reactivity Package
- More info
How Does Vue3 Do This?
detect when there's a change in one of the valuesproxies 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!
- can do other things before return:
- 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
bindsthis
properly ***
- ***
- end up with the same thing...so why would we do it?
- 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
andtrigger
: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!)
- a map is a series of keys and values, similar to an object, but with some differences:
WeakMap()
- similar to
Map()
, but the references are held weakly --- meaning, if you delete something the reference can be garbage collected (in aMap()
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
- similar to
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