- Intro to Vue 3
- Creating the Vue App
- Attribute Binding
- Conditional Rendering
- List Rendering
- Event Handling
- Class and Style Binding
- Computed Properties
- Components and Props
- Communicating Events
- Forms and v-model
- VueMastery Video
- Covers fundamentals of building a Vue app
- Administer variations of product
- Add to cart / remove from cart logic
- In stock / out of stock logic
- Build a review form with Vue
- Recommended extension for VSCode:
- es6-string-html by Tobermory
-
Create Vue app:
π main.js
const app = Vue.createApp({ data() { return { product: 'Socks' } `}` })
-
Mount Vue app:
π index.html
<html> <body> <div id="app"></div> <!-- Import App --> <script src="./main.js"></script> <!-- Mount App --> <script>const mountedApp = app.mount('#app')</script> </body> </html>
-
Once the app is mounted, you can run JavaScript Expression inside html file such as:
<h1>{{ product }}</h1> <p>{{ firstName + ' ' + lastName }}</p> <span>{{ clicked ? true : false }}</span> <div>{{ message.method() }}</div>
Reactivity System underneath the hood will take care of the DOM updates. Learn more here >> Reactivity in Depth
-
Attribute binding example:
<img v-bind:src="image"> <img :src="image"> <!-- equivalent shorthand -->
-
More examples:
<img :src="image"> <img :alt="description">1 <a :href="url"> <div :class="isActive"> <span :style="isActive"> <span :disables="isDisabled">
-
Directives: v-if | v-else | v-show
π main.js
const app = Vue.createApp({ data() { return { product: 'Socks', image: './assets/images/socks_blue.jpg', inStock: true } } })
π index.html
<p v-if="inStock">In Stock</p> <p v-else>Out of Stock</p>
<p v-show="inStock">In Stock</p>
π main.js
const app = Vue.createApp({ data() { return { ... inventory: 100 }
π index.html
<p v-if="inventory > 10">In Stock</p> <p v-else>Out of Stock</p>
<p v-if="inventory > 10">In Stock</p> <p v-else-if="inventory <= 10 && inventory > 0">Almost sold out!</p> <p v-else>Out of Stock</p>
-
Looping through an array list and array of objects: v-for
π main.js
const app = Vue.createApp({ data() { return { ... details: ['50% cotton', '30% wool', '20% polyester'] } } })
π index.html
<ul> <li v-for="detail in details">{{ detail }}</li> </ul>
π main.js
data() { return { ... variants: [ { id: 2234, color: 'green' }, { id: 2235, color: 'blue' } ] } }
π index.html
<div v-for="variant in variants" :key="variant.id">{{ variant.color }}</div>
-
Event Handling
π index.html
<div class="cart">Cart({{ cart }})</div> ... <button class="button">Add to Cart</button>
π main.js
data() { return { cart: 0, ... } }
π index.html
<button class="button" v-on:click="run logic here">Add to Cart</button>
Call the method on click:
<button class="button" v-on:click="addToCart">Add to Cart</button>
π main.js
const app = Vue.createApp({ data() { return { cart: 0, ... } }, methods: { addToCart() { this.cart += 1 } } })
Shorthand for 'v-on' is '@'
<button class="button" @click="addToCart">Add to Cart</button>
π main.js
data() { return { ... variants: [ { id: 2234, color: 'green', image: './assets/images/socks_green.jpg' }, { id: 2235, color: 'blue', image: './assets/images/socks_blue.jpg' }, ] } }
π index.html
<div v-for="variant in variants" :key="variant.id" @mouseover="updateImage(variant.image)">{{ variant.color }}</div>
π main.js
methods: { ... updateImage(variantImage) { this.image = variantImage } }
-
Style Binding :open_file_folder: index.html
<div v-for="variant in variants" :key="variant.id" @mouseover="updateImage(variant.image)" class="color-circle" </div>
π style.css
.color-circle { width: 50px; height: 50px; margin-top: 8px; border: 2px solid #d8d8d8; border-radius: 50%; }
π index.html
<div v-for="variant in variants" :key="variant.id" @mouseover="updateImage(variant.image)" class="color-circle" :style="{ backgroundColor: variant.color }"> </div>
Understanding Style Binding
<div ... :style="{ backgroundColor: variant.color }"> </div>
Use Camel case for css property inside JavaScript Expression:
<div :style="{ backgroundColor: variant.color }></div>
Add quotes if you want to use Kebab case:
<div :style="{ 'background-color': variant.color }></div>
-
Style Binding: Objects :open_file_folder: index.html
<div :style="styles"></div>
π main.js
data() { return { styles: { color: 'green', fontSize: '14px' } } }
-
Class Binding
-
Disable 'add to cart' when out of stock
-
Make button look disabled
π index.html
<button class="button" :disabled="!inStock" v-on:click="addToCart"> Add to Cart </button>
π style.css
.disabledButton { background-color: #d8d8d8; cursor: not-allowed; }
-
Conditionally apply the class when out of stock
π index.html
<button class="button" :class="{ disabledButton: !inStock }" :disabled="!inStock" @click="addToCart"> Add to Cart </button>
-
-
Multiple Class Names Conditionally add the
active
classπ index.html
<div class="color-circle" :class="{ active: activeClass }"> </div>
π main.js
data() { return { activeClass: true } }
Combined look:
<div class="color-circle active"></div>
-
Ternary Operators
In-line example:
<div :class="[ isActive ? activeClass : '' ]"></div>
-
Simple Computed Property
π main.js
data() { return { product: 'Socks', brand: 'Vue Mastery' }
π index.html
<h1>{{ brand + ' ' + product }}</h1>
Compute the Brand and Product Together: :open_file_folder: index.html
<h1>{{ title }}</h1>
π main.js
... computed: { title() { return this.brand + ' ' + this.product } }
-
Computing Image & Quantity Add
quantity
property to the variant objectsπ main.js
data() { return { ... variants: [ { id: 2234, color: 'green', image: './assets/images/socks_green.jpg', quantity: 50 }, { id: 2235, color: 'blue', image: './assets/images/socks_blue.jpg', quantity: 0 }, ] } }
Replace
updateImage()
with new methodupdateVariant()
π index.html<div v-for="(variant, index) in variants" :key="variant.id" @mouseover="updateVariant(index)" <! -- new method --> class="color-circle" :style="{ backgroundColor: variant.color }"> </div>
Add new data property which will be updated to equal
index
π main.jsdata() { return { ... selectedVariant: 0, ... } }
π main.js
updateVariant(index) { this.selectedVariant = index }
Delete
image
&inStock
from data, replace with computed properties :open_file_folder: main.jscomputed: { ... image() { return this.variants[this.selectedVariant].image }, inStock() { return this.variants[this.selectedVariant].quantity } }
-
Create ProductDisplay component :open_file_folder: components/ProductDisplay.js
app.component('product-display', {})
-
Template :open_file_folder: components/ProductDisplay.js
app.component('product-display', { template: /*html*/ `<div class="product-display"> <div class="product-container"> <div class="product-image"> <img v-bind:src="image"> </div> <div class="product-info"> <h1>{{ title }}</h1> <p v-if="inStock">In Stock</p> <p v-else>Out of Stock</p> <div v-for="(variant, index) in variants" :key="variant.id" @mouseover="updateVariant(index)" class="color-circle" :style="{ backgroundColor: variant.color }"> </div> <button class="button" :class="{ disabledButton: !inStock }" :disabled="!inStock" v-on:click="addToCart"> Add to Cart </button> </div> </div> </div>` })
Notice the template literal back ticks
``
, /*html*/ & VSCode extension:es6-string-html, syntax highlighting should work in VSCode. -
Move
data
&methods
frommain.js
to hereπ components/ProductDisplay.js
app.component('product-display', { template: /*html*/ `<div class="product-display"> ... </div>`, data() { return { product: 'Socks', brand: 'Vue Mastery', selectedVariant: 0, details: ['50% cotton', '30% wool', '20% polyester'], variants: [ { id: 2234, color: 'green', image: './assets/images/socks_green.jpg', quantity: 50 }, { id: 2235, color: 'blue', image: './assets/images/socks_blue.jpg', quantity: 0 }, ] } }, methods: { addToCart() { this.cart += 1 }, updateVariant(index) { this.selectedVariant = index } }, computed: { title() { return this.brand + ' ' + this.product }, image() { return this.variants[this.selectedVariant].image }, inStock() { return this.variants[this.selectedVariant].quantity } } })
-
Cleaning up
main.js
- leaving only cart data & empty methods for nowconst app = Vue.createApp({ data() { return { cart: 0, } }, methods: {} })
-
Importing the Component
π index.html
<!-- Import Components --> <script src="./components/ProductDisplay.js"></script>
-
Using it in the template:
π index.html
<div id="app"> <div class="nav-bar"></div> <div class="cart">Cart({{ cart }})</div> <product-display></product-display> </div>
-
Example of reusable blocks - add two more
product-display
componentsπ index.html
<div id="app"> <div class="nav-bar"></div> <div class="cart">Cart({{ cart }})</div> <product-display></product-display> <product-display></product-display> <product-display></product-display> </div>
- props are custom attributes for passing data into a component.
Giving component a prop - premium account
-
set the type & if it's required
π main.js
const app = Vue.createApp({ data() { return { cart: 0, premium: true } } })
π components/ProductDisplay.js
app.component('product-display', { props: { premium: { type: Boolean, required: true } }, ... }
-
Add custom attribute to
product-display
component
π index.html
<div id="app">
<div class="nav-bar"></div>
<div class="cart">Cart({{ cart }})</div>
<product-display :premium="premium"></product-display>
</div>
- Conditionally display shipping cost - if
premium
istrue
, shipping is free, otherwise, $2.99
π components/ProductDisplay.js
template:
/*html*/
`<div class="product-display">
...
<p>Shipping: {{ shipping }}</p>
...
</div>`,
π components/ProductDisplay.js
computed: {
...
shipping() {
if (this.premium) {
return 'Free'
}
return 2.99
}
}
-
Emitting the event - from product display component to
main.js
so we can update the cartπ components/ProductDisplay.js
methods: { addToCart() { this.$emit('add-to-cart') } ... }
π index.html
<product-display :premium="premium" @add-to-cart="updateCart"></product-display>
-
Add
updateCart()
method inmain.js
π main.js
const app = Vue.createApp({ data() { return { cart: [], ... } }, methods: { updateCart() { this.cart += 1 } } })
-
Add product
id
to the cartπ main.js
const app = Vue.createApp({ data() { return { cart: [], ... } }, methods: { updateCart(id) { this.cart.push(id) } }
-
Add a payload to
add-to-cart
event emission, soupdateCart
has access to thatid
π components/ProductDisplay.js
methods: { addToCart() { this.$emit('add-to-cart', this.variants[this.selectedVariant].id) } ... }
-
Since no need to display the
id
in the cart, we can use.length
method to display the amount of items instead<div id="app"> ... <div class="cart">Cart({{ cart.length }})</div> ... </div>
-
Add a Product Review Form with Two-way Binding:
v-model
-
Create new component - ReviewForm.js
π components/ReviewForm.js
app.component('review-form', { template: /*html*/ `<form class="review-form"> <h3>Leave a review</h3> <label for="name">Name:</label> <input id="name"> <label for="review">Review:</label> <textarea id="review"></textarea> <label for="rating">Rating:</label> <select id="rating"> <option>5</option> <option>4</option> <option>3</option> <option>2</option> <option>1</option> </select> <input class="button" type="submit" value="Submit"> </form>`, data() { return { name: '', review: '', rating: null } } })
-
Inside of the template, notice these elements:
<input id="name">
<textarea id="review">
<select id="rating">
-
Bind these input fields to their respective data properties
data() { return { name: '', review: '', rating: null } }
-
Add
v-model
directive to each of these input elementsπ components/ReviewForm.js
app.component('review-form', { template: /*html*/ `<form class="review-form"> <h3>Leave a review</h3> <label for="name">Name:</label> <input id="name" v-model="name"> <label for="review">Review:</label> <textarea id="review" v-model="review"></textarea> <label for="rating">Rating:</label> <select id="rating" v-model.number="rating"> <option>5</option> <option>4</option> <option>3</option> <option>2</option> <option>1</option> </select> <input class="button" type="submit" value="Submit"> </form>`, data() { return { name: '', review: '', rating: null } })
Notice how on the <select> element, we used
v-model.number
this is a modifier that typecasts the value as a number. -
Submitting the Review Form
-
Add a listener
π components/ReviewForm.js
app.component('review-form', { template: /*html*/ `<form class="review-form" @submit.prevent="onSubmit"> ... <input class="button" type="submit" value="Submit"> </form>` ... })
-
Add
onSubmit()
methodπ components/ReviewForm.js
... data() { return { name: '', review: '', rating: null } }, methods: { onSubmit() { let productReview = { name: this.name, review: this.review, rating: this.rating, } this.$emit('review-submitted', productReview) this.name = '' this.review = '' this.rating = null } } ...
-
Import the Review Form
π index.html
<!-- Import Components --> ... <script src="./components/ReviewForm.js"></script> ...
-
Use
review-form
component inside Product Displayπ components/ProductDisplay.js
template: /*html*/ `<div class="product-display"> <div class="product-container"> ... </div> <review-form></review-form> </div>` })
-
-
Adding Product Reviews
-
Add event listener onto the
review-form
π components/ProductDisplay.js
template: /*html*/ `<div class="product-display"> <div class="product-container"> ... </div> <review-form @review-submitted="addReview"></review-form> </div>` })
-
Add a new
addReview()
methodπ components/ProductDisplay.js
... data() { return { ... reviews: [] } }, methods: { ... addReview(review) { this.reviews.push(review) } }, ...
-
-
Displaying the reviews
-
Create new component - ReviewList.js to show the product review
π components/ReviewList.js
app.component('review-list', { props: { reviews: { type: Array, required: true } }, template: /*html*/ ` <div class="review-container"> <h3>Reviews:</h3> <ul> <li v-for="(review, index) in reviews" :key="index"> {{ review.name }} gave this {{ review.rating }} stars <br/> "{{ review.review }}" <br/> </li> </ul> </div> ` })
-
Import this new component
π index.html
<!-- Import Components --> ... <script src="./components/ReviewList.js"></script> ...
π components/ProductDisplay.js
template: /*html*/ `<div class="product-display"> <div class="product-container"> ... </div> <review-list :reviews="reviews"></review-list> <review-form @review submitted="addReview"></review-form> </div>` })
-
-
Don't show the review if none is present using
.length
methodπ components/ProductDisplay.js
template: /*html*/ `<div class="product-display"> ... <review-list v-if="reviews.length" :reviews="reviews"></review-list> ... </div>` })
-
Basic Form Validation
π components/ReviewForm.js
methods: { onSubmit() { if (this.name === '' || this.review === '' || this.rating === null) { alert('Review is incomplete. Please fill out every field.') return } ... } }
- Last Modified On: 2021-08-09
- Creation Date: 2021-08-05