A Comprehensive Guide to Vue.js Framework Fundamentals
What is Vue.js?
Vue.js is a progressive framework for building user interfaces
Vue.js is a data-driven progressive framework
一. Your First Vue Instance
1.1 Steps
1.1.1 Building the User Interface (Frontend Page) — Creating a Vue Instance, Initializing Rendering
- Prepare a container, either a div tag or other tag element as the container
- Import the package - after importing, the Vue constructor becomes available globally, allowing new Vue()
- Create Vue instance new Vue()
- Specify configuration options — Rendering Data
① el specifies the mount point, determining which container the Vue instance manages
② data provides the data
These two configuration options determine where and what data to display in the container
<!-- Container -->
<div id="app">
{{message}}
</div>
<!-- Local Import -->
<script src="../day01/files/vue.js" ></script>
<!-- CDN Import -->
<!-- <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> -->
<script>
const app = new Vue({
el : '#app',
data : {
message : 'Tomorrow will be better!'
}
})
</script>
2. Interpolation Expressions {{ expression }}
- Interpolation is a Vue template syntax
- Purpose: Use expressions to interpolate and render content to the page
- Important notes:
- ① Data used in interpolation must exist in the data option
- ② Interpolation supports expressions only (expressions must produce a result), not statements - cannot use if statements in interpolation
- ③ Interpolation cannot be used in tag attributes
2.1 Common Interpolation Errors
- When using undefined data 'xyz' not in data, console error:
Property or method "xyz" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
The property or method "xyz" was not defined on the instance but was referenced during rendering. Ensure the property is reactive by initializing it in the data option or for class-based components.
- When using if statements in interpolation, page won't render, console error:
Error compiling template:
avoid using JavaScript keyword as property name: "if"
Template compilation error:
Avoid using JavaScript keyword as property name: "if"
- When using interpolation in tag attributes, console error:
Error compiling template:
title="{{msg}}": Interpolation inside attributes has been removed. Use v-bind or the colon shorthand instead. For example, instead of <div id="{{ val }}">, use <div :id="val">.
Template compilation error:
title="{{msg}}": Interpolation inside attributes has been removed. Use v-bind or the colon shorthand instead.
2.2 Vue's Core Feature: Reactivity — When data changes, the view automatically updates
How to access or modify data in data?
Data in data gets added to the Vue instance: const app = new Vue({})
① Accessing data: "instance.propertyName"
② Modifying data: "instance.propertyName" = "newValue"
Browser Console Data Access and Modification
2.3 Installing Vue DevTools
2.4 Vue Directives
Vue applies different functionality to tags based on various directives
Directives: Special tag attributes with v- prefix
- v-html: Sets element's innerHTML, parsing tags Syntax: v-html = "expression"
<!-- Container --> <div id="app"> <div v-html="content"></div> </div> <!-- Local Import --> <script src="../day01/files/vue.js" ></script> <!-- CDN Import --> <!-- <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> --> <script> const app = new Vue({ el : '#app', data : { content : '<h1>Tomorrow will be better!</h1>', quantity : '100' } }) </script>
1. v-show and v-if
Implementation
This is a v-show controlled box
This is a v-if controlled box
const app = new Vue({
el : '#app',
data : {
content : 'Tomorrow will be better!',
> quantity : '100',
> isVisible : true
> }
> })
>
Result: Expression evaluates to false
Result: Expression evaluates to true
2.v-else and v-else-if
- v-else and v-else-if assist v-if in conditional rendering; v-if must be present
- v-else has no expression, only v-else-if has an expression
I will become better
Grade A
Grade B
Grade C
Grade D
const app = new Vue({
el : '#app',
data : {
// If value is null, defaults to false
isGood : '',
score : '75'
}
})
Error when using v-else/v-else-if without v-if
Error compiling template:
v-else-if="score > 80" used on element <li> without corresponding v-if.
3 | <div>
4 | <!-- <li v-if="score > 90">Grade A</li> -->
5 | <li v-else-if="score > 80">Grade B</li>
| ^^^^^^^^^^^^^^^^^^^^^^
6 | <li v-else-if="score >70">Grade C</li>
7 | <li v-else="">Grade D</li>Template compilation error:
v-else-if="score > 80" used on element <li> without corresponding v-if.
When compilation errors occur, check syntax
2.v-on:eventName = "inline handler"
Implementation
-
{{quantity}}
+
const app = new Vue({
el : '#app',
data : {
quantity : '100',
}
})
If event name is missing, error occurs
v-on without argument expects an Object value
v-on without argument expects an Object value
v-on:eventName = "methodName from methods option"
Implementation
Toggle Visibility
Visible
const app = new Vue({
el : '#app',
data : {
isVisible : true,
},
methods : {
toggle(){
// app.isVisible = !app.isVisible
this.isVisible = !this.isVisible
}
}
})
Key Points
- methods cannot directly access data via property names because data and methods are at the same level with local scope
- How to use data in methods? Data is mounted on the Vue instance, accessed via VueInstance.propertyName, i.e., app.isVisible = !app.isVisible
- In methods, this refers to the Vue instance — use this.propertyName to access data properties, i.e., this.isVisible = !this.isVisible
Error when accessing undefined data
Error in v-on handler: "ReferenceError: isVisible is not defined"
Error in v-on handler: "ReferenceError: isVisible is not defined"
Passing Arguments with v-on: @click(arg1, arg2, ...)
Vending Machine
Milk $10
Congee $7
Balance: ${{balance}}
const app = new Vue({
el : '#app',
data : {
balance : "100",
},
methods : {
purchace(cost){
this.balance = this.balance - cost
}
}
})
Error when function receives arguments but doesn't use them
Error in v-on handler: "ReferenceError: cost is not defined"
Error in v-on handler: "ReferenceError: cost is not defined"
3. v-bind
Implementation
Previous
Next
const app = new Vue({
el : '#app',
data : {
currentIndex : 0,
images : [
'img/001.webp',
'img/002.webp',
'img/003.webp',
'img/004.webp',
]
},
})
4. v-for
Implementation
{{item}}
const app = new Vue({
el : '#app',
data : {
currentIndex : 0,
items : [
'Banana',
'Pineapple',
'Apple'
]
},
})
Example:
{{item.title}}
Delete
const app = new Vue({
el : '#app',
data : {
bookList : [
{id:1,title:'Dream of Red Chamber',author:'Cao Xueqin'},
{id:2,title:'Journey to the West',author:'Wu Chengen'},
{id:3,title:'Water Margin',author:'Shi Nai\'an'},
{id:4,title:'Romance of Three Kingdoms',author:'Luo Guanzhong'},
]
},
methods:{
remove(id){
/* function(item){
return item.id !==id
}
In JavaScript, arrays have a filter() method that creates a new array with all elements that pass the test
*/
this.bookList = this.bookList.filter(entry => entry.id !== id)
}
}
})
Key in v-for
Difference with and without key can be observed through CSS styling
Without key: deleting Dream of Red Chamber, style is preserved (li not actually removed)
With key: deleting Dream of Red Chamber, style is also removed
5 . v-model
Variable is defined in data option
Implementation
Username:
Password:
Login
Reset
const app = new Vue({
el : '#app',
data : {
username : '',
password : '',
},
methods:{
clearForm(){
this.username='',
this.password=''
}
}
})
6. Directive Modifiers
Vue directive modifiers essentially wrap event objects
Error when using modifiers incorrectly, correct: v-on:click.prevent="handler" wrong: v-on.prevent:click="handler"
v-on without argument does not support modifiers
v-on without argument does not support modifiers
7.v-bind for class Binding
Array syntax for binding class names requires quotes v-bind:class ="['class1','class2']"
Implementation
v-bind with object syntax for CSS
v-bind with array syntax for CSS
const app = new Vue({
el : '#app',
data : {
username : '',
password : '',
},
methods:{
clearForm(){
this.username='',
this.password=''
}
}
})
.yellowBg {
background-color: yellow;
}
.largeText {
font-size: 25px;
}
.box {
width: 700px;
height: 800px;
}
.boxWrapper{
width: 500px;
height: 600px;
}
.boxWrapperColor{
background-color: aqua;
}
Example: Flash Sale Tab Navigation Highlighting
Example Code
Document
* {
margin: 0;
padding: 0;
}
ul {
display: flex;
border-bottom: 2px solid #e01222;
padding: 0 10px;
}
li {
width: 100px;
height: 50px;
line-height: 50px;
list-style: none;
text-align: center;
}
li a {
display: block;
text-decoration: none;
font-weight: bold;
color: #333333;
}
li a.active {
background-color: #e01222;
color: #fff;
}
{{item.label}}
const app = new Vue({
el: '#app',
data: {
activeIndex : 0,
tabs: [
{ id: 1, label: 'Flash Sale' },
{ id: 2, label: 'Daily Deals' },
{ id: 3, label: 'Category Deals' }
]
}
})
8.v-bind for style Binding
When binding styles with v-bind, CSS property names and values must follow JS syntax
- background-color should be written as 'background-color' or backgroundColor
- CSS values must be in quotes
- v-bind:style="{ 'background-color' : 'red' }"
Example Implementation
Document
{{percent}}%
Set 25%
Set 50%
Set 75%
Set 100%
const app = new Vue({
el: '#app',
data: {
percent : 0,
}
})
.progress {
height: 25px;
width: 400px;
border-radius: 15px;
background-color: #272425;
border: 3px solid #272425;
box-sizing: border-box;
margin-bottom: 30px;
}
.inner {
width: 50%;
height: 20px;
border-radius: 10px;
text-align: right;
position: relative;
background-color: #409eff;
background-size: 20px 20px;
box-sizing: border-box;
transition: all 1s;
}
.inner span {
position: absolute;
right: -20px;
bottom: -25px;
}
9. v-model with Other Form Elements
Document
textarea {
display: block;
width: 240px;
height: 100px;
margin: 10px 0;
}
Learning Platform
Name:
Single:
Gender:
Male
Female
City:
Beijing
Shanghai
Chengdu
Nanjing
Bio:
Register
const app = new Vue({
el: '#app',
data: {
fullname:'',
isSingle:true,
gender:'1',
cityId:'1',
bio:'',
}
})
10. Computed Properties
- Based on existing data, derive new properties - if dependent data changes, computed properties recalculate. Example: existing data 1, 2, 5 → computed data 8
- Computed properties are declared in the computed option, each computed property corresponds to a function
- Usage is the same as regular properties {{ computedPropertyName }}
- Computed properties are essentially properties, no parentheses when using (not calling), the name is the function name in computed option
Implementation
Document
table {
border: 1px solid #000;
text-align: center;
width: 240px;
}
th,td {
border: 1px solid #000;
}
h3 {
position: relative;
}
Gift List
Name
Quantity
{{ item.name }}
{{ item.quantity }} pcs
Total Items: {{totalItems}} pcs
const app = new Vue({
el: '#app',
data: {
// Existing data
gifts: [
{ id: 1, name: 'Basketball', quantity: 1 },
{ id: 2, name: 'Toys', quantity: 2 },
{ id: 3, name: 'Pencils', quantity: 5 },
]
},
computed:{
totalItems(){
let sum = this.gifts.reduce((acc,item) =>acc+item.quantity ,0)
return sum;
}
}
})
Difference Between Computed Properties and Methods
Computed Property Full Syntax
- get() Gets the computed property value
- set() Sets the computed property value, affecting the underlying data
Implementation
Document
First Name:+
Last Name:={{fullName}}
Update Name
const app = new Vue({
el: '#app',
data: {
firstName:'Huang',
lastName :'fu'
},
computed: {
fullName:{
get(){
return this.firstName + this.lastName
},
set(value){
this.firstName = value.slice(0,1)
this.lastName = value.slice(1)
}
}
},
methods: {
updateName(){
this.fullName = 'Andy Lau'
}
}
})
- When assigning to a computed property, the value passes to set(value) as parameter
- Example: Assigning to computed property => this.fullName = 'Andy Lau', value in set(value) is 'Andy Lau'
Problem: Assigning to computed property without set(), console error
Computed property "fullName" was assigned to but it has no setter.
Computed property "fullName" was assigned to but it has no setter.
Grade Example:
- filter: Details http://t.csdnimg.cn/61oOH
- reduce : Details http://t.csdnimg.cn/xY8Hk
Document
ID
Subject
Score
Action
{{index + 1}}
{{item.subject}}
{{item.score}}
Delete
No Data
Total: {{totalScore}}
Average: {{averageScore}}
Subject:
Score:
Add
const app = new Vue({
el: '#app',
data: {
grades: [
{ id: 1, subject: 'Chinese', score: 20 },
{ id: 7, subject: 'Math', score: 99 },
{ id: 12, subject: 'English', score: 70 },
],
subject: '',
score: ''
},
methods:{
remove(id){
this.grades = this.grades.filter(item => item.id !== id)
},
addGrade(){
if(!this.subject){
alert("Subject cannot be empty")
return
}
if(typeof this.score !== 'number'){
alert("Please enter valid score")
return
}
this.grades.unshift({
id : +new Date(),
subject : this.subject,
score : this.score,
})
this.subject = ''
this.score = ''
}
},
computed : {
totalScore(){
// reduce((accumulative result, item) => result after each calculation, initial value)
return this.grades.reduce((acc,item) =>acc+item.score,0)
},
averageScore(){
// toFixed keeps two decimal places
return (this.totalScore / this.grades.length).toFixed(2)
}
}
})
11.watch Watchers
- When watched properties or data change, automatically execute related logic operations
Implementation
Document
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-size: 18px;
}
#app {
padding: 10px 20px;
}
.query {
margin: 10px 0;
}
.box {
display: flex;
}
textarea {
width: 300px;
height: 160px;
font-size: 18px;
border: 1px solid #dedede;
outline: none;
resize: none;
padding: 10px;
}
textarea:hover {
border: 1px solid #1589f5;
}
.transbox {
width: 300px;
height: 160px;
background-color: #f0f0f0;
padding: 10px;
border: none;
}
.tip-box {
width: 300px;
height: 25px;
line-height: 25px;
display: flex;
}
.tip-box span {
flex: 1;
text-align: center;
}
.query span {
font-size: 18px;
}
.input-wrap {
position: relative;
}
.input-wrap span {
position: absolute;
right: 15px;
bottom: 15px;
font-size: 12px;
}
.input-wrap i {
font-size: 20px;
font-style: normal;
}
Translate to:
Italian
English
German
⌨️Document Translation
mela
// API: https://applet-base-api-t.itheima.net/api/translate
// Method: get
// Parameters:
// (1) words: text to translate (required)
// (2) lang: target language (optional) default - Italian
// -----------------------------------------------
const app = new Vue({
el: '#app',
data: {
inputText: '',
obj : {
// quotes required
'obj.wordsObj' : ''
}
},
watch : {
// watching data properties, usually only newValue needed
inputText(newValue,oldValue){
console.log(newValue+" new value, old value " + oldValue);
},
// watching object properties
'obj.wordsObj'(newValue,oldValue){
console.log(newValue+" new value, old value " + oldValue);
}
},
// Details: (1) watch syntax (2) business implementation
})
Complete Watcher Syntax
- When needing to watch entire object properties, using property name(newValue, oldValue) requires multiple watchers
- Now watching an object to watch all properties, configure two options watch : { objectName : {
- deep : true
- handler(newValue, oldValue){ } }
- }
- Any property change triggers handler function
- immediate option, when configured, executes handler once immediately without waiting for change
Example: Shopping Cart
Shopping Cart
🏠
/
Cart
Select
Image
Price
Quantity
Subtotal
Action
{{item.price}}
-
{{item.quantity}}
+
{{item.price * item.quantity}}
Delete
Select All
Total : ¥ {{totalAmount}}
Checkout( {{selectedCount}} )
🛒Cart is empty
const app = new Vue({
el: '#app',
data: {
// Product list
productList: [
{
id: 1,
image: 'img/Pitaya.png',
isChecked: true,
quantity: 2,
price: 6,
},
{
id: 2,
image: 'img/Lychee.png',
isChecked: false,
quantity: 7,
price: 20,
},
{
id: 3,
image: 'img/Durian.png',
isChecked: false,
quantity: 3,
price: 40,
},
{
id: 4,
image: 'img/Pear.png',
isChecked: true,
quantity: 10,
price: 3,
},
{
id: 5,
image: 'img/Cherry.png',
isChecked: false,
quantity: 20,
price: 34,
},
],
},
methods : {
removeItem(id){
this.productList = this.productList.filter((item) =>item.id !==id )
},
increment(id){
const product = this.productList.find(item => item.id === id)
product.quantity++
},
decrement(id){
const product = this.productList.find(item => item.id === id)
product.quantity--
}
},
computed:{
selectAll:{
get(){
// All checkboxes must be checked for select all --every
return this.productList.every(item => item.isChecked === true)
},
set(value){
this.productList.forEach(element => {
element.isChecked =value
});
},
},
///////////////////////////////////
selectedCount(){
return this.productList.reduce((acc,item) => {
if(item.isChecked){
//Selected
return acc + item.quantity
}else{
return acc
}
},0)
},
////////////////////////////////////
totalAmount(){
return this.productList.reduce((acc,item) => {
if(item.isChecked){
return acc + item.quantity * item.price
}else{
return acc
}
},0)
}
},
watch : {
productList : {
deep : true,
handler (newValue) {
localStorage.setItem('list',JSON.stringify(newValue))
}
}
}
})
二. Lifecycle
Lifecycle Summary: Four phases, eight functions
- Creation and mounting phases execute once, update phase executes multiple times. During update phase, when data changes, view updates - this is a loop
Four Phases
Eight Functions
- beforeCreate executes before creation phase, like standing before the creation phase
- created means data is already reactive
- Lifecycle hooks are at the same level as data option
Implementation
Document
{{ title }}
-
{{ count }}
+
const app = new Vue({
el: '#app',
data: {
count: 100,
title: 'Counter'
},
// 1. Creation phase (preparing data)
beforeCreate () {
console.log('beforeCreate before reactive data is ready', this.count)
},
created () {
console.log('created after reactive data is ready', this.count)
// this.dataName = fetched data
// Can start initialization requests
},
// 2. Mounting phase (rendering template)
beforeMount () {
console.log('beforeMount before template renders', document.querySelector('h3').innerHTML)
},
mounted () {
console.log('mounted after template renders', document.querySelector('h3').innerHTML)
// Can manipulate DOM now
},
// 3. Update phase (modify data → update view)
beforeUpdate () {
console.log('beforeUpdate data modified, view not yet updated', document.querySelector('span').innerHTML)
},
updated () {
console.log('updated data modified, view already updated', document.querySelector('span').innerHTML)
},
// 4. Unmount phase
beforeDestroy () {
console.log('beforeDestroy, before unmount')
console.log('Clean up external resources, timers, delays...')
},
destroyed () {
console.log('destroyed, after unmount')
}
})
Console Screenshot
Example
Document
.red {
color: red!important;
}
.search {
width: 300px;
margin: 20px 0;
}
.my-form {
display: flex;
margin: 20px 0;
}
.my-form input {
flex: 1;
margin-right: 20px;
}
.table > :not(:first-child) {
border-top: none;
}
.contain {
display: flex;
padding: 10px;
}
.list-box {
flex: 1;
padding: 0 30px;
}
.list-box a {
text-decoration: none;
}
.echarts-box {
width: 600px;
height: 400px;
padding: 30px;
margin: 0 auto;
border: 1px solid #ccc;
}
tfoot {
font-weight: bold;
}
@media screen and (max-width: 1000px) {
.contain {
flex-wrap: wrap;
}
.list-box {
width: 100%;
}
.echarts-box {
margin-top: 30px;
}
}
Add Bill
ID
Expense Name
Amount
Action
{{ index + 1 }}
{{ item.name }}
{{ item.price.toFixed(2) }}
Delete
Total: {{ totalAmount.toFixed(2) }}
/**
* API Documentation:
* https://www.apifox.cn/apidoc/shared-24459455-ebb1-4fdc-8df8-0aff8dc317a8/api-53371058
*
* Features:
* 1. Basic rendering
* (1) Immediately fetch data on created
* (2) Store data in reactive data option
* (3) Render with v-for
* (4) Total calculation => computed property
* 2. Add feature
* (1) Collect form data with v-model
* (2) Click handler sends add request
* (3) Re-render
* 3. Delete feature
* (1) Click handler passes id
* (2) Send delete request based on id
* (3) Re-render
* 4. Pie chart rendering
* (1) Initialize chart with echarts.init(dom) in mounted
* (2) Update chart with echarts.setOption({ ... })
*/
const app = new Vue({
el: '#app',
data: {
expenseList: [],
name: '',
price: ''
},
computed: {
totalAmount () {
return this.expenseList.reduce((acc, item) => acc + item.price, 0)
}
},
created () {
this.fetchExpenses()
},
mounted () {
this.myChart = echarts.init(document.querySelector('#main'))
this.myChart.setOption({
// Title
title: {
text: 'Expense List',
left: 'center'
},
// Tooltip
tooltip: {
trigger: 'item'
},
// Legend
legend: {
orient: 'vertical',
left: 'left'
},
// Series data
series: [
{
name: 'Expenses',
type: 'pie',
radius: '50%',
data: [
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
},
methods: {
async fetchExpenses () {
const res = await axios.get('https://applet-base-api-t.itheima.net/bill', {
params: {
creator: 'User'
}
})
this.expenseList = res.data.data
// Update chart
this.myChart.setOption({
series: [
{
data: this.expenseList.map(item => ({ value: item.price, name: item.name}))
}
]
})
},
async addEntry () {
if (!this.name) {
alert('Please enter expense name')
return
}
if (typeof this.price !== 'number') {
alert('Please enter valid amount')
return
}
// Send add request
const res = await axios.post('https://applet-base-api-t.itheima.net/bill', {
creator: 'User',
name: this.name,
price: this.price
})
// Re-render
this.fetchExpenses()
this.name = ''
this.price = ''
},
async removeEntry (id) {
await axios.delete(`https://applet-base-api-t.itheima.net/bill/${id}`)
this.fetchExpenses()
}
}
})
三. Scaffolding and Project Structure
1. Vue CLI Scaffolding
2. Standard Directory Structure
Note: When opening project with VSCode, open from root directory
Root Component Overview
// Export is the component options object
// Can include data(special) methods computed watch lifecycle hooks
export default {
created () {
console.log('created hook')
},
methods: {
handleClick () {
alert('Hello')
}
}
}
/* Enable less support in style
1. Add lang="less" to style
2. Install dependencies less less-loader
yarn add less less-loader -D (dev dependency)
*/
.App {
width: 400px;
height: 400px;
background-color: pink;
.box {
width: 100px;
height: 100px;
background-color: skyblue;
}
}
main.js Overview
// Core function: Import App.vue, render structure based on App.vue into index.html
// 1. Import Vue core library
import Vue from 'vue'
// 2. Import App.vue root component
import App from './App.vue'
// Note: Environment indicator (production/development)
Vue.config.productionTip = false
// 3. Vue instance, provide render method → render structure based on App.vue into index.html
new Vue({
// el: '#app', same as $mount('#app'), specifies container
// render: h => h(App),
render: (createElement) => {
return createElement(App)
}
}).$mount('#app')
3. Component-Based Development
4. Three Parts of Components
When style has nested CSS, set style lang to less
- Need to install dependencies
- yarn add less less-loader -D ----- -D development dependency
- Nested CSS
- <style lang="less">
- .APP{
- .box {
- }
- }
- <style>
5.Component Registration
1. Local Component Registration
Tab completion setting: Settings → search trigger on tab → enable
Implementation: Local Registration
import HeaderComponent from './components/HeaderComponent.vue'
import MainComponent from './components/MainComponent.vue'
import FooterComponent from './components/FooterComponent.vue'
export default {
name: 'App',
components: {
Header: HeaderComponent,
Main: MainComponent,
// Same name, can abbreviate
FooterComponent
}
}
2. Global Component Registration
- In main.js, one file can only register one global component
- Vue.component('componentName', importedComponentObject)
四. Component Communication
1. Three Component Parts Explained
- Default component styles are global and affect other components. In practice, each component should have independent styles using <style scoped></style>
2. Component's data Option Must Be a Function — This ensures each execution returns a new data object, so multiple instances don't affect each other
.vue component options are objects
3. Component Communication
1. Parent to Child
- In parent component, pass value via attribute on child component tag
- In child component, receive via props
- Use in child component template
Implementation
App.vue root component
import Child from './components/Child.vue'
export default {
name: 'App',
data() {
return {
parentTitle : 'Component Communication: Parent to Child'
}
},
components: {
Child : Child
}
}
Child.vue component
I'm Child component {{title}}
export default {
name: 'ChildComponent',
// 2. Receive via props, array values must match bound attribute names
props : ['title']
}
2. Child to Parent
Child.vue component
I'm Child component {{title}}
Child to Parent Communication
export default {
name: 'ChildComponent',
props: ['title'],
methods: {
sendToParent() {
// 2. Use this.$emit() to notify parent first param is custom event name used in parent binding,
// second param is new value to pass
this.$emit("updateTitle",'Tech University')
}
}
}
App.vue parent component
import Child from './components/Child.vue'
export default {
name: 'App',
data() {
return {
parentTitle : 'Component Communication: Parent to Child'
}
},
methods: {
// 4. newValue is the updated value --> Tech University
handleUpdate(newTitle) {
this.parentTitle = newTitle
}
},
components: {
Child : Child
}
}
Notes
- vm.$emit("customEventName", "newValue") First param: custom event name used as binding event in parent, second param: data passed from child to parent
3.Props
property attributes
Implementation
App.vue
import UserInfo from './components/UserInfo.vue'
export default {
name: 'App',
data() {
return {
username: 'Handsome',
age: 28,
isSingle: true,
car: {
brand: 'BMW',
},
hobbies: ['Basketball', 'Soccer', 'Badminton'],
}
},
components: {
UserInfo : UserInfo
}
}
UserInfo.vue
Personal Info Component
Name: {{username}}
Age: {{age}}
Single: {{isSingle}}
Car: {{car.brand}}
Hobbies: {{hobbies.join('、')}}
export default {
props:['username','age','isSingle','car','hobbies']
}
.userinfo {
width: 300px;
border: 3px solid #000;
padding: 20px;
}
.userinfo > div {
margin: 20px 10px;
}
Props Validation
- Property name : Data type --> Type validation, validates if passed value type matches
App.vue code
import ProgressBar from './components/ProgressBar.vue'
export default {
data() {
return {
progressWidth: 50,
}
},
components: {
ProgressBar:ProgressBar,
},
methods: {
receiveWidthUpdate(newValue) {
this.progressWidth = newValue
}
}
}
ProgressBar.vue code
{{ width }}%
Change
export default {
// 1. Basic syntax (type validation)
// props: {
// width: Number,
// },
// 2. Full syntax (type, default, required, custom validator)
props: {
width: {
type: Number,
required: true,
default: 0,
// validator(val) param val is the passed value
validator(val) {
if (val >= 100 || val
localStorage -> read on load
export default {
data() {
return {
todos: JSON.parse(localStorage.getItem('todos')) || [
{ id: 1, name: 'Play Basketball' },
{ id: 2, name: 'Watch Movie' },
{ id: 3, name: 'Go Shopping' },
],
}
},
components: {
TodoInput,
TodoList,
TodoFooter,
},
watch: {
todos: {
deep: true,
handler(newVal) {
localStorage.setItem('todos', JSON.stringify(newVal))
},
},
},
methods: {
handleAdd(taskName) {
this.todos.unshift({
id: +new Date(),
name: taskName,
})
},
handleDelete(id) {
this.todos = this.todos.filter((item) => item.id !== id)
},
clearAll() {
this.todos = []
},
},
}
TodoInput.vue
Notes App
Add Task
export default {
data(){
return {
taskName:''
}
},
methods:{
handleAdd(){
this.$emit('add',this.taskName)
this.taskName = ''
}
}
}
TodoList.vue
{{ index + 1 }}.
{{ item.name }}
export default {
props: {
todos: {
type: Array,
},
},
methods: {
handleDelete(id) {
this.$emit('delete', id)
},
},
}
TodoFooter.vue
Total: {{ todos.length }}
Clear All
export default {
props: {
todos: {
type: Array,
},
},
methods:{
clearAll(){
this.$emit('clear')
}
}
}
4. Non-Parent-Child Communication
4.1 Event Bus
Implementation
1. Create utils/EventBus.js file
// Event bus, create Vue instance
import Vue from 'vue'
const Bus = new Vue()
export default Bus
2. App.vue code
import ComponentA from './components/ComponentA.vue'
import ComponentB from './components/ComponentB.vue'
import ComponentC from './components/ComponentC.vue'
export default {
components:{
ComponentA,
ComponentB,
ComponentC
}
}
3. ComponentB.vue sender code
I'm Component B (Publisher)
Send Message
import Bus from '../utils/EventBus'
export default {
methods: {
sendMessage() {
// vm.$emit() first param is custom event name, must match in receiver
// vm.$emit() second param is data, use param in receiver
Bus.$emit('sendMsg', 'Nice weather today, good for travel')
},
},
}
.component-b {
width: 200px;
height: 200px;
border: 3px solid #000;
border-radius: 3px;
margin: 10px;
}
4. ComponentA.vue receiver code
I'm Component B (Publisher)
Send Message
import Bus from '../utils/EventBus'
export default {
methods: {
sendMessage() {
Bus.$emit('sendMsg', 'Nice weather today, good for travel')
},
},
}
.component-b {
width: 200px;
height: 200px;
border: 3px solid #000;
border-radius: 3px;
margin: 10px;
}
5. ComponentC.vue receiver code
I'm Component C (Receiver)
{{message}}
import Bus from '../utils/EventBus'
export default {
data() {
return {
message: '',
}
},
created() {
Bus.$on('sendMsg', (msg) => {
this.message = msg
})
},
}
.component-c {
width: 200px;
height: 200px;
border: 3px solid #000;
border-radius: 3px;
margin: 10px;
}
Event bus is one-to-many: one component publishes, others that receive correctly get the data
4.2 Cross-Level Data Transmission (grandparent-grandchild, etc.)
- provide: Supply; specify; give --> As data sender
- inject: Add, inject --> As data receiver
1. app.vue code
I'm App Component
Change Data
import ChildA from './components/ChildA.vue'
import ChildB from './components/ChildB.vue'
export default {
provide() {
return {
// Simple type is NOT reactive, this.color is just a value
color: this.color,
// Complex type IS reactive, this.userData is an object
userData: this.userData,
}
},
data() {
return {
color: 'pink',
userData: {
name: 'John',
age: 18,
},
}
},
methods: {
change() {
this.color = 'red'
this.userData.name = 'Jane'
},
},
components: {
ChildA,
ChildB,
},
}
.app {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
}
2. ChildA.vue code
I'm ChildA Component
import Grandchild from '../components/Grandchild.vue'
export default {
components:{
Grandchild
}
}
.ChildA {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
height: 200px;
}
3. ChildB.vue code
I'm ChildB Component
export default {
}
.ChildB {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
height: 200px;
}
4. Grandchild.vue is child of ChildA.vue
I'm Grandchild
{{ color }} -{{ userData.name }} -{{ userData.age }}
export default {
// Receiver, variable names must match provide declarations
inject: ['color', 'userData'],
}
.grandchild {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
height: 100px;
}
5.v-model Deep Dive
6.Form Component Encapsulation (Parent-Child Two-Way Binding)
- v-model can only implement two-way binding on own data
App.vue code
import CitySelect from './components/CitySelect.vue'
export default {
data() {
return {
currentCityId: '102',
}
},
components: {
CitySelect,
},
methods: {
handleCityChange(val) {
console.log(val);
this.currentCityId = val
}
}
}
CitySelect.vue code
Beijing
Shanghai
Wuhan
Guangzhou
Shenzhen
export default {
props: ['selectedCityId'],
data() {
return {
selectedValue: this.selectedCityId,
}
},
methods: {
onCityChange(e) {
this.$emit("cityChange",e.target.value)
}
}
}
7.Form Component Encapsulation (Simplified with v-model)
1.When child uses props value (value) to receive from parent and custom event name is input, can use v-model in parent to simplify
App.vue code
import CitySelect from './components/CitySelect.vue'
export default {
data() {
return {
currentCityId: '102',
}
},
components: {
CitySelect,
},
methods: {
handleCityChange(val) {
console.log(val);
this.currentCityId = val
}
}
}
CitySelect.vue code
Beijing
Shanghai
Wuhan
Guangzhou
Shenzhen
export default {
props: {
value : String
},
methods: {
onCityChange(e) {
this.$emit("input",e.target.value)
}
}
}
8.Summary
4.3 sync Modifier
- sync allows custom prop names
App.vue
Logout
import Dialog from "./components/Dialog.vue"
export default {
data() {
return {
isDialogVisible : false,
}
},
methods: {
openDialog() {
this.isDialogVisible=true
}
},
components: {
Dialog,
},
}
Dialog.vue code
Notice:
x
Are you sure you want to logout?
Confirm
Cancel
export default {
props: {
visible : Boolean,
},
methods: {
closeDialog() {
// Must write update:visible
// visible is custom prop name from props
this.$emit("update:visible",false)
}
}
}
.dialog-wrapper {
width: 300px;
height: 200px;
box-shadow: 2px 2px 2px 2px #ccc;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: 0 10px;
}
.dialog .title {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #000;
}
.dialog .content {
margin-top: 38px;
}
.dialog .title .close {
width: 20px;
height: 20px;
cursor: pointer;
line-height: 10px;
}
.footer {
display: flex;
justify-content: flex-end;
margin-top: 26px;
}
.footer button {
width: 80px;
height: 40px;
}
.footer button:nth-child(1) {
margin-right: 10px;
cursor: pointer;
}
4.4 ref and $refs (Accessing DOM and Component Instances)
- document.querySelector finds all elements, $refs only searches current component
4.4.1 ref and $refs for DOM Access
App.vue
import ChartComponent from './components/ChartComponent.vue'
export default {
components:{
ChartComponent
}
}
.chart-box {
width: 200px;
height: 100px;
}
ChartComponent.vue
Child Component
import * as echarts from 'echarts'
export default {
mounted() {
// Initialize echarts based on prepared dom
// Get ref corresponding DOM element
const chart = echarts.init(this.$refs.myChart)
// Draw chart
chart.setOption({
title: {
text: 'ECharts Quick Start',
},
tooltip: {},
xAxis: {
data: ['Shirt', 'Wool Shirt', 'Blouse', 'Pants', 'Heels', 'Socks'],
},
yAxis: {},
series: [
{
name: 'Sales',
type: 'bar',
data: [5, 20, 36, 10, 10, 20],
},
],
})
},
}
.chart-box {
width: 400px;
height: 300px;
border: 3px solid #000;
border-radius: 6px;
}
4.4.2 ref and $refs for Component Instance Access
App.vue
Get Data
Reset
import LoginForm from './components/LoginForm.vue'
export default {
components: {
LoginForm,
},
methods: {
getData() {
console.log(this.$refs.LoginForm);
console.log(this.$refs.LoginForm.getFormValues());
},
resetData() {
this.$refs.LoginForm.clearForm()
}
}
}
LoginForm.vue code
Username:
Password:
export default {
data() {
return {
username: 'admin',
password: '123456',
}
},
methods: {
getFormValues() {
return {
username: this.username,
password : this.password
}
},
clearForm() {
this.username = ''
this.password = ''
console.log('Form reset successfully');
},
}
}
.app {
border: 2px solid #ccc;
padding: 10px;
}
.app div{
margin: 10px 0;
}
.app div button{
margin-right: 8px;
}
4.5 Vue Async Updates and $nextTick
App.vue code
Confirm
{{ heading }}
Edit
export default {
data() {
return {
heading: 'Main Heading',
showEdit: false,
editValue: '',
}
},
methods: {
handleEdit() {
this.showEdit = true
// Function body executes after DOM updates
this.$nextTick( () => {
this.$refs.inputRef.focus()
})
}
},
}
5.1. Custom Directives
5.1.1 Global Directive Registration
**In main.js, configure global directives
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
Vue.directive('focus', {
// inserted triggers when directive's element is inserted into page
inserted (el) {
el.focus()
}
})
new Vue({
render: h => h(App),
}).$mount('#app')
App.vue use custom directive v-focus
Custom Directives
export default {
}
5.1.2 Local Custom Directives
Defined within component, can only be used in that component
App.vue code
Custom Directives
export default {
// Local directive registration
directives: {
// Directive name: directive config
focus: {
inserted (el) {
el.focus()
}
}
}
}
5.1.3 Custom Directive Values
App.vue code
Directive Value Test 1
Directive Value Test 2
export default {
data () {
return {
color1: 'red',
color2: 'orange'
}
},
directives: {
color: {
// inserted provides logic when element is added to page
inserted (el, binding) {
// binding.value is the directive value
el.style.color = binding.value
},
// update triggers when directive value changes, provides logic after DOM update
update (el, binding) {
console.log('Directive value changed');
el.style.color = binding.value
}
}
}
}
5.1.4 Custom Loading Directive
App.vue
{{ item.title }}
{{ item.source }}
{{ item.time }}
import axios from 'axios'
export default {
data () {
return {
newsList: [],
isLoading: true,
}
},
async created () {
const res = await axios.get('http://hmajax.itheima.net/api/news')
setTimeout(() => {
this.newsList = res.data.data
this.isLoading = false
}, 2000)
},
directives: {
loading: {
inserted (el, binding) {
binding.value ? el.classList.add('loading') : el.classList.remove('loading')
},
update (el, binding) {
binding.value ? el.classList.add('loading') : el.classList.remove('loading')
}
}
}
}
.loading:before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #fff url('./loading.gif') no-repeat center;
}
.box2 {
width: 400px;
height: 400px;
border: 2px solid #000;
position: relative;
}
.box {
width: 800px;
min-height: 500px;
border: 3px solid orange;
border-radius: 5px;
position: relative;
}
.news {
display: flex;
height: 120px;
width: 600px;
margin: 0 auto;
padding: 20px 0;
cursor: pointer;
}
.news .left {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding-right: 10px;
}
.news .left .title {
font-size: 20px;
}
.news .left .info {
color: #999999;
}
.news .left .info span {
margin-right: 20px;
}
.news .right {
width: 160px;
height: 120px;
}
.news .right img {
width: 100%;
height: 100%;
object-fit: cover;
}
5.2 Slots
- Slots allow customized effects on wrapped components
- Use slots to customize structure of wrapped components
App.vue code
Are you sure you want to delete?
Are you sure you want to exit?
import ModalDialog from './components/ModalDialog.vue'
export default {
data () {
return {
}
},
components: {
ModalDialog
}
}
body {
background-color: #b3b3b3;
}
ModalDialog.vue
Notice
✖️
Cancel
Confirm
export default {
data () {
return {
}
}
}
* {
margin: 0;
padding: 0;
}
.dialog {
width: 470px;
height: 230px;
padding: 0 25px;
background-color: #ffffff;
margin: 40px auto;
border-radius: 5px;
}
.dialog-header {
height: 70px;
line-height: 70px;
font-size: 20px;
border-bottom: 1px solid #ccc;
position: relative;
}
.dialog-header .close {
position: absolute;
right: 0px;
top: 0px;
cursor: pointer;
}
.dialog-content {
height: 80px;
font-size: 18px;
padding: 15px 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
.dialog-footer button {
width: 65px;
height: 35px;
background-color: #ffffff;
border: 1px solid #e1e3e9;
cursor: pointer;
outline: none;
margin-left: 10px;
border-radius: 3px;
}
.dialog-footer button:last-child {
background-color: #007acc;
color: #fff;
}
5.2.1 Slot Fallback Content
App.vue
Confirm exit?
import ModalDialog from './components/ModalDialog.vue'
export default {
data () {
return {
}
},
components: {
ModalDialog
}
}
body {
background-color: #b3b3b3;
}
ModalDialog.vue
Notice
✖️
Fallback: Default Content
Cancel
Confirm
export default {
data() {
return {}
},
}
* {
margin: 0;
padding: 0;
}
.dialog {
width: 470px;
height: 230px;
padding: 0 25px;
background-color: #ffffff;
margin: 40px auto;
border-radius: 5px;
}
.dialog-header {
height: 70px;
line-height: 70px;
font-size: 20px;
border-bottom: 1px solid #ccc;
position: relative;
}
.dialog-header .close {
position: absolute;
right: 0px;
top: 0px;
cursor: pointer;
}
.dialog-content {
height: 80px;
font-size: 18px;
padding: 15px 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
.dialog-footer button {
width: 65px;
height: 35px;
background-color: #ffffff;
border: 1px solid #e1e3e9;
cursor: pointer;
outline: none;
margin-left: 10px;
border-radius: 3px;
}
.dialog-footer button:last-child {
background-color: #007acc;
color: #fff;
}
5.2.2 Named Slots
App.vue code
Named Slot Header
Named Slot Content
Confirm
Cancel
import ModalDialog from './components/ModalDialog.vue'
export default {
data () {
return {
}
},
components: {
ModalDialog
}
}
body {
background-color: #b3b3b3;
}
ModalDialog.vue code
Title
Content
Buttons
export default {
data() {
return {}
},
}
* {
margin: 0;
padding: 0;
}
.dialog {
width: 470px;
height: 230px;
padding: 0 25px;
background-color: #ffffff;
margin: 40px auto;
border-radius: 5px;
}
.dialog-header {
height: 70px;
line-height: 70px;
font-size: 20px;
border-bottom: 1px solid #ccc;
position: relative;
}
.dialog-header .close {
position: absolute;
right: 0px;
top: 0px;
cursor: pointer;
}
.dialog-content {
height: 80px;
font-size: 18px;
padding: 15px 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
.dialog-footer button {
width: 65px;
height: 35px;
background-color: #ffffff;
border: 1px solid #e1e3e9;
cursor: pointer;
outline: none;
margin-left: 10px;
border-radius: 3px;
}
.dialog-footer button:last-child {
background-color: #007acc;
color: #fff;
}
5.2.3 Scoped Slots
- row is custom object name
- Scoped slots can pass data
App.vue code
Delete
View
import DataTable from './components/DataTable.vue'
export default {
data () {
return {
list1: [
{ id: 1, name: 'Zhang Xiao', age: 18 },
{ id: 2, name: 'Sun Daming', age: 19 },
{ id: 3, name: 'Liu Dezhong', age: 17 },
],
list2: [
{ id: 1, name: 'Zhao Xiaoyun', age: 18 },
{ id: 2, name: 'Liu Beibei', age: 19 },
{ id: 3, name: 'Jiang Xiaotai', age: 17 },
]
}
},
methods: {
deleteRow (id) {
this.list1 = this.list1.filter(item => item.id !== id)
},
viewRow (row) {
alert(`Name: ${row.name}; Age: ${row.age}`)
}
},
components: {
DataTable
}
}
DataTable.vue code
ID
Name
Age
Action
{{ index + 1 }}
{{ item.name }}
{{ item.age }}
export default {
props: {
data: Array
}
}
.data-table {
width: 450px;
text-align: center;
border: 1px solid #ccc;
font-size: 24px;
margin: 30px auto;
}
.data-table thead {
background-color: #1f74ff;
color: #fff;
}
.data-table thead th {
font-weight: normal;
}
.data-table thead tr {
line-height: 40px;
}
.data-table th,
.data-table td {
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
}
.data-table td:last-child {
border-right: none;
}
.data-table tr:last-child td {
border-bottom: none;
}
.data-table button {
width: 65px;
height: 35px;
font-size: 18px;
border: 1px solid #ccc;
outline: none;
border-radius: 3px;
cursor: pointer;
background-color: #ffffff;
margin-left: 5px;
}
5.3 Example: Product List
main.js: Focus directive
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
Vue.directive('focus', {
inserted (el) {
el.focus()
}
})
new Vue({
render: h => h(App),
}).$mount('#app')
Table.vue code
export default {
props: {
data: {
type: Array,
required: true
}
}
};
.custom-table {
width: 100%;
border-spacing: 0;
img {
width: 100px;
height: 100px;
object-fit: contain;
vertical-align: middle;
}
th {
background: #f5f5f5;
border-bottom: 2px solid #069;
}
td {
border-bottom: 1px dashed #ccc;
}
td,
th {
text-align: center;
padding: 10px;
transition: all .5s;
&.red {
color: red;
}
}
.none {
height: 100px;
line-height: 100px;
color: #999;
}
}
TagInput.vue code
{{ inputValue }}
export default {
props: {
inputValue: String
},
data () {
return {
isEditing: false
}
},
methods: {
handleClick () {
this.isEditing = true
},
handleEnter (e) {
if (e.target.value.trim() === '') return alert('Tag cannot be empty')
this.$emit('input', e.target.value)
this.isEditing = false
}
}
}
.tag-input {
cursor: pointer;
.input {
appearance: none;
outline: none;
border: 1px solid #ccc;
width: 100px;
height: 40px;
box-sizing: border-box;
padding: 10px;
color: #666;
&::placeholder {
color: #666;
}
}
}
App.vue code
ID
Name
Image
Tag
{{ index + 1 }}
{{ item.name }}
import TagInput from './components/TagInput.vue'
import ProductTable from './components/ProductTable.vue'
export default {
name: 'TableCase',
components: {
TagInput,
ProductTable
},
data () {
return {
products: [
{ id: 101, image: 'https://...', name: 'Purple Clay Teapot', tag: 'Tea Set' },
{ id: 102, image: 'https://...', name: 'Waterproof Hiking Shoes', tag: 'Men Shoes' },
{ id: 103, image: 'https://...', name: 'Teddy Bear Vest', tag: 'Kids Clothing' },
{ id: 104, image: 'https://...', name: 'Knit Sweater', tag: 'Kids Clothing' },
]
}
}
}
.table-case {
width: 1000px;
margin: 50px auto;
img {
width: 100px;
height: 100px;
object-fit: contain;
vertical-align: middle;
}
}
- Install less: npm install less@4 --save-dev
- Install less-loader: npm install less-loader@7 --save-dev
- Choose appropriate versions based on build tool version
5.3 Single Page Application
5.4 Vue Router
5.4.1 Vue Router
Version Compatibility (Vue VueRouter Vuex)
- Vue2 VueRouter3.x Vuex3.x
- Vue3 VueRouter4.x Vuex4.x
Vue Router can be installed via npm
Documentation: Installation | Vue Router (vuejs.org)
npm install vue-router@4
5.4.2 Configure Global Router in main.js
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
Vue.config.productionTip = false
Vue.use(VueRouter)
const router = new VueRouter()
new Vue({
render: h => h(App),
router: router
}).$mount('#app')
5.4.5 Router Configuration
main.js code
import Vue from 'vue'
import App from './App.vue'
// Router usage: 5 + 2 steps
// 5 basic steps
// 1. Install v3.6.5
// 2. Import
// 3. Install Vue.use(Vue plugin)
// 4. Create router instance
// 5. Inject into new Vue, establish connection
// 2 core steps
// 1. Create components(views folder), configure rules
// 2. Prepare navigation links, configure router outlet
import Discover from './views/Discover'
import Mine from './views/Mine'
import Friends from './views/Friends'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
// routes route rules
// route one rule is an object { path: path, component: component }
routes: [
{ path: '/discover', component: Discover },
{ path: '/mine', component: Mine },
{ path: '/friends', component: Friends },
]
})
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router
}).$mount('#app')
App.vue code
Discover Music
My Music
Friends
export default {};
body {
margin: 0;
padding: 0;
}
.footer_wrap {
position: relative;
left: 0;
top: 0;
display: flex;
width: 100%;
text-align: center;
background-color: #333;
color: #ccc;
}
.footer_wrap a {
flex: 1;
text-decoration: none;
padding: 20px 0;
line-height: 20px;
background-color: #333;
color: #ccc;
border: 1px solid black;
}
.footer_wrap a:hover {
background-color: #555;
}
Mine.vue code
My Music
My Music
My Music
My Music
export default {
name: 'MyMusic'
}
Discover.vue code
Discover Music
Discover Music
Discover Music
Discover Music
export default {
name: 'DiscoverMusic'
}
Friends.vue code
My Friends
My Friends
My Friends
My Friends
export default {
name: 'MyFriends'
}
5.4.5 Component Directory Organization
5.4.3 Router Module Encapsulation
main.js code
import Vue from 'vue'
import App from './App.vue'
import router from './router/index'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router
}).$mount('#app')
router/index.js code
import Discover from '@/views/Discover'
import Mine from '@/views/Mine'
import Friends from '@/views/Friends'
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/discover', component: Discover },
{ path: '/mine', component: Mine },
{ path: '/friends', component: Friends },
]
})
export default router
5.5 Declarative Navigation
- <router-link to="/configured path">
router/index.js code
import Discover from '@/views/Discover'
import Mine from '@/views/Mine'
import Friends from '@/views/Friends'
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/discover', component: Discover },
{ path: '/mine', component: Mine },
{ path: '/friends', component: Friends },
]
})
export default router
main.js code
import Vue from 'vue'
import App from './App.vue'
import router from './router/index'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router
}).$mount('#app')
App.vue code
Discover Music
My Music
Friends
export default {};
body {
margin: 0;
padding: 0;
}
.footer_wrap {
position: relative;
left: 0;
top: 0;
display: flex;
width: 100%;
text-align: center;
background-color: #333;
color: #ccc;
}
.footer_wrap a {
flex: 1;
text-decoration: none;
padding: 20px 0;
line-height: 20px;
background-color: #333;
color: #ccc;
border: 1px solid black;
}
.footer_wrap a.router-link-active {
background-color: purple;
}
.footer_wrap a:hover {
background-color: #555;
}
5.5.1 Exact and Fuzzy Matching
5.5.1 Custom Class Names
router/index.js code
import Discover from '@/views/Discover'
import Mine from '@/views/Mine'
import Friends from '@/views/Friends'
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/discover', component: Discover },
{ path: '/mine', component: Mine },
{ path: '/friends', component: Friends },
],
linkActiveClass: 'active',
linkExactActiveClass: 'exact-active'
})
export default router
App.vue code
Discover Music
My Music
Friends
export default {};
body {
margin: 0;
padding: 0;
}
.footer_wrap {
position: relative;
left: 0;
top: 0;
display: flex;
width: 100%;
text-align: center;
background-color: #333;
color: #ccc;
}
.footer_wrap a {
flex: 1;
text-decoration: none;
padding: 20px 0;
line-height: 20px;
background-color: #333;
color: #ccc;
border: 1px solid black;
}
.footer_wrap a.active {
background-color: purple;
}
.footer_wrap a:hover {
background-color: #555;
}
5.5.2 Declarative Navigation - Query Parameters
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router/index'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router
}).$mount('#app')
router/index.js code
import HomePage from '@/views/HomePage'
import SearchPage from '@/views/SearchPage'
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/home', component: HomePage },
{ path: '/search', component: SearchPage }
]
})
export default router
App.vue code
Home
Search
export default {};
.link {
height: 50px;
line-height: 50px;
background-color: #495150;
display: flex;
margin: -8px -8px 0 -8px;
margin-bottom: 50px;
}
.link a {
display: block;
text-decoration: none;
background-color: #ad2a26;
width: 100px;
text-align: center;
margin-right: 5px;
color: #fff;
border-radius: 5px;
}
views/HomePage.vue code
Search
Hot Searches:
Tech Academy
Frontend Training
How to Become Expert
export default {
name: 'HomePage'
}
.logo-box {
height: 150px;
background: url('@/assets/logo.jpeg') no-repeat center;
}
.search-box {
display: flex;
justify-content: center;
}
.search-box input {
width: 400px;
height: 30px;
line-height: 30px;
border: 2px solid #c4c7ce;
border-radius: 4px 0 0 4px;
outline: none;
}
.search-box input:focus {
border: 2px solid #ad2a26;
}
.search-box button {
width: 100px;
height: 36px;
border: none;
background-color: #ad2a26;
color: #fff;
position: relative;
left: -2px;
border-radius: 0 4px 4px 0;
}
.hot-link {
width: 508px;
height: 60px;
line-height: 60px;
margin: 0 auto;
}
.hot-link a {
margin: 0 5px;
}
views/SearchPage.vue code
Search Keyword: {{ $route.query.keyword }}
Results:
.............
.............
.............
.............
export default {
name: 'SearchPage',
created () {
console.log(this.$route.query.keyword);
}
}
.search {
width: 400px;
height: 240px;
padding: 0 20px;
margin: 0 auto;
border: 2px solid #c4c7ce;
border-radius: 5px;
}
5.5.3 Declarative Navigation - Dynamic Route Parameters
router/index.js code
import HomePage from '@/views/HomePage'
import SearchPage from '@/views/SearchPage'
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/home', component: HomePage },
// Dynamic route /search/:term - :term is parameter, term is custom name
{ path: '/search/:term', component: SearchPage }
]
})
export default router
main.js code
import Vue from 'vue'
import App from './App.vue'
import router from './router/index'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router
}).$mount('#app')
App.vue
Home
Search
export default {};
.link {
height: 50px;
line-height: 50px;
background-color: #495150;
display: flex;
margin: -8px -8px 0 -8px;
margin-bottom: 50px;
}
.link a {
display: block;
text-decoration: none;
background-color: #ad2a26;
width: 100px;
text-align: center;
margin-right: 5px;
color: #fff;
border-radius: 5px;
}
views/HomePage.vue
Search
Hot Searches:
Tech Academy
Frontend Training
How to Become Expert
export default {
name: 'HomePage'
}
.logo-box {
height: 150px;
background: url('@/assets/logo.jpeg') no-repeat center;
}
.search-box {
display: flex;
justify-content: center;
}
.search-box input {
width: 400px;
height: 30px;
line-height: 30px;
border: 2px solid #c4c7ce;
border-radius: 4px 0 0 4px;
outline: none;
}
.search-box input:focus {
border: 2px solid #ad2a26;
}
.search-box button {
width: 100px;
height: 36px;
border: none;
background-color: #ad2a26;
color: #fff;
position: relative;
left: -2px;
border-radius: 0 4px 4px 0;
}
.hot-link {
width: 508px;
height: 60px;
line-height: 60px;
margin: 0 auto;
}
.hot-link a {
margin: 0 5px;
}
views/SearchPage.vue
Search Term: {{ $route.params.term }}
Results:
.............
.............
.............
.............
export default {
name: 'SearchPage',
created () {
console.log(this.$route.params.term);
}
}
.search {
width: 400px;
height: 240px;
padding: 0 20px;
margin: 0 auto;
border: 2px solid #c4c7ce;
border-radius: 5px;
}
- :parameterName means parameter is required, shows blank if not passed
- :parameterName? means parameter optional, page renders normally without it
5.5.4 Route Redirect
router/index.js code
import HomePage from '@/views/HomePage'
import SearchPage from '@/views/SearchPage'
import NotFound from '@/views/NotFound'
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', redirect: '/home' },
{ path: '/home', component: HomePage },
{ name: 'search', path: '/search/:term?', component: SearchPage },
{ path: '*', component: NotFound }
]
})
export default router
5.5.5 Basic Programmatic Navigation
router/index.js code
import HomePage from '@/views/HomePage'
import SearchPage from '@/views/SearchPage'
import NotFound from '@/views/NotFound'
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', redirect: '/home' },
{ path: '/home', component: HomePage },
{ name: 'search', path: '/search/:term?', component: SearchPage },
{ path: '*', component: NotFound }
]
})
export default router
views/HomePage.vue code
Search
Hot Searches:
Tech Academy
Frontend Training
How to Become Expert
export default {
name: 'HomePage',
methods: {
goSearch () {
this.$router.push({
name: 'search'
})
}
}
}
.logo-box {
height: 150px;
background: url('@/assets/logo.jpeg') no-repeat center;
}
.search-box {
display: flex;
justify-content: center;
}
.search-box input {
width: 400px;
height: 30px;
line-height: 30px;
border: 2px solid #c4c7ce;
border-radius: 4px 0 0 4px;
outline: none;
}
.search-box input:focus {
border: 2px solid #ad2a26;
}
.search-box button {
width: 100px;
height: 36px;
border: none;
background-color: #ad2a26;
color: #fff;
position: relative;
left: -2px;
border-radius: 0 4px 4px 0;
}
.hot-link {
width: 508px;
height: 60px;
line-height: 60px;
margin: 0 auto;
}
.hot-link a {
margin: 0 5px;
}
5.5.6 Programmatic Navigation - Route Params
router/index.js code
import HomePage from '@/views/HomePage'
import SearchPage from '@/views/SearchPage'
import NotFound from '@/views/NotFound'
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', redirect: '/home' },
{ path: '/home', component: HomePage },
{ name: 'search', path: '/search/:term?', component: SearchPage },
{ path: '*', component: NotFound }
]
})
export default router
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router/index'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router
}).$mount('#app')
App.vue
Home
Search
export default {};
.link {
height: 50px;
line-height: 50px;
background-color: #495150;
display: flex;
margin: -8px -8px 0 -8px;
margin-bottom: 50px;
}
.link a {
display: block;
text-decoration: none;
background-color: #ad2a26;
width: 100px;
text-align: center;
margin-right: 5px;
color: #fff;
border-radius: 5px;
}
views/HomePage.vue
Search
Hot Searches:
Tech Academy
Frontend Training
How to Become Expert
export default {
name: 'HomePage',
data () {
return {
searchTerm: ''
}
},
methods: {
goSearch () {
this.$router.push({
name: 'search',
params: {
term: this.searchTerm
}
})
}
}
}
.logo-box {
height: 150px;
background: url('@/assets/logo.jpeg') no-repeat center;
}
.search-box {
display: flex;
justify-content: center;
}
.search-box input {
width: 400px;
height: 30px;
line-height: 30px;
border: 2px solid #c4c7ce;
border-radius: 4px 0 0 4px;
outline: none;
}
.search-box input:focus {
border: 2px solid #ad2a26;
}
.search-box button {
width: 100px;
height: 36px;
border: none;
background-color: #ad2a26;
color: #fff;
position: relative;
left: -2px;
border-radius: 0 4px 4px 0;
}
.hot-link {
width: 508px;
height: 60px;
line-height: 60px;
margin: 0 auto;
}
.hot-link a {
margin: 0 5px;
}
views/SearchPage.vue
Search Term: {{ $route.params.term }}
Results:
.............
.............
.............
.............
export default {
name: 'SearchPage',
created () {
}
}
.search {
width: 400px;
height: 240px;
padding: 0 20px;
margin: 0 auto;
border: 2px solid #c4c7ce;
border-radius: 5px;
}
views/NotFound.vue
404 Not Found
export default {
}
5.5.7 Component Caching
router/index.js code
import Vue from 'vue'
import VueRouter from "vue-router";
import Layout from '@/views/Layout'
import ArticleList from '@/views/ArticleList'
import Favorites from '@/views/Favorites'
import Likes from '@/views/Likes'
import Profile from '@/views/Profile'
import ArticleDetail from '@/views/ArticleDetail'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{
path: '/',
component: Layout,
redirect: '/article',
children: [
{
path: '/article',
component: ArticleList
},
{
path: '/favorites',
component: Favorites
},
{
path: '/likes',
component: Likes,
},
{
path: '/profile',
component: Profile
}
]
},
{
path: '/detail/:id',
component: ArticleDetail
}
]
})
export default router
main.js code
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router
}).$mount('#app')
App.vue
export default {
name: 'h5-wrapper',
data () {
return {
keepArr: ['LayoutPage']
}
}
}
body{
margin: 0;
padding: 0;
}
.h5-wrapper {
.content {
margin-bottom: 51px;
}
.tabbar {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 50px;
line-height: 50px;
text-align: center;
display: flex;
background: #fff;
border-top: 1px solid #e4e4e4;
a {
flex: 1;
text-decoration: none;
font-size: 14px;
color: #333;
&.router-link-active {
color: #fa0;
}
}
}
}
ArticleList.vue
{{ item.title }}
{{ item.authorName }} | {{ item.date }}
{{ item.summary }}
Likes {{ item.likes }} | Views {{ item.views }}
import axios from 'axios'
export default {
name: 'ArticleListPage',
data () {
return {
articles: []
}
},
async created () {
const res = await axios.get('https://mock.boxuegu.com/mock/3083/articles')
this.articles = res.data.result.rows
}
}
.article-list {
background: #f5f5f5;
}
.article-item {
margin-bottom: 10px;
background: #fff;
padding: 10px 15px;
.head {
display: flex;
img {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
}
.con {
flex: 1;
overflow: hidden;
padding-left: 15px;
p {
margin: 0;
line-height: 1.5;
&.title {
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
white-space: nowrap;
}
&.other {
font-size: 10px;
color: #999;
}
}
}
}
.body {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-top: 10px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.foot {
font-size: 12px;
color: #999;
margin-top: 10px;
}
}
ArticleDetail.vue
< Article Detail
{{ article.title }}
{{ article.date }} | {{ article.views }} Views | {{ article.likes }} Likes
{{ article.authorName }}
{{ article.content }}
import axios from 'axios'
export default {
name: 'ArticleDetailPage',
data () {
return {
article: {}
}
},
async created () {
const id = this.$route.params.id
const { data } = await axios.get(`https://mock.boxuegu.com/mock/3083/articles/${id}`)
this.article = data.result
}
};
.article-detail {
.nav {
height: 44px;
border-bottom: 1px solid #e4e4e4;
line-height: 44px;
text-align: center;
.back {
font-size: 18px;
color: #666;
position: absolute;
left: 10px;
top: 0;
transform: scale(1, 1.5);
}
}
.header {
padding: 0 15px;
p {
color: #999;
font-size: 12px;
display: flex;
align-items: center;
}
img {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
}
}
.body {
padding: 0 15px;
}
}
Layout.vue
Articles
Favorites
Likes
Profile
export default {
name: 'LayoutPage',
created () {
console.log('created component loaded');
},
mounted () {
console.log('mounted DOM rendered');
},
destroyed () {
console.log('destroyed component destroyed');
},
activated () {
alert('Welcome back')
console.log('activated component activated');
},
deactivated () {
console.log('deactivated component deactivated');
}
}
body{
margin: 0;
padding: 0;
}
.h5-wrapper {
.content {
margin-bottom: 51px;
}
.tabbar {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 50px;
line-height: 50px;
text-align: center;
display: flex;
background: #fff;
border-top: 1px solid #e4e4e4;
a {
flex: 1;
text-decoration: none;
font-size: 14px;
color: #333;
}
a.router-link-active {
color: orange;
}
}
}
5.6 Creating Projects with Vue CLI
- Create new Vue project
vue create project-name
- Select Manually select features
- Select features
- Choose Vue version
- Router mode
- Use history mode for router?
- Choose CSS pre-processor
- Choose ESLint config
- Pick lint features
- Where to place config
- Save preset
5.7 ESLint Code Standards
- JavaScript Standard Style https://standardjs.com/rules-zhcn.html
- ESLint Rules: https://eslint.org/docs/latest/rules
Auto-fix Configuration
In settings
- Vuex