Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

A Comprehensive Guide to Vue.js Framework Fundamentals

Tech May 16 19

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

  1. Prepare a container, either a div tag or other tag element as the container
  2. Impport the package - after importing, the Vue constructor becomes available globally, allowing new Vue()
  3. Create Vue instance new Vue()
  4. 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

  1. 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.

  1. 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"

  1. 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
<!-- Container  -->

This is a v-show controlled box
This is a v-if controlled box


<!-- Local Import -->

<!-- CDN Import -->
<!--  -->

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
<!-- Container  -->

I will become better

90">Grade A
80">Grade B
70">Grade C
Grade D



<!-- Local Import -->

<!-- CDN Import -->
<!--  -->

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
<!-- Container  -->

-
{{quantity}}
+


<!-- Local Import -->

<!-- CDN Import -->
<!--  -->

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
<!-- Container  -->

Toggle Visibility
Visible


<!-- Local Import -->


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, ...)
<!-- Container  -->



Vending Machine

Milk $10

Congee $7

Balance: ${{balance}}




<!-- Local Import -->


const app = new Vue({
el : '#app',
data : {
balance : "100",
},
methods : {
purchase(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

<!-- Container  -->

0" v-on:click="currentIndex--" style="width: 200px;height: 200px;">Previous


![]()


Next



<!-- Local Import -->


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

<!-- Container  -->

- {{item}}



<!-- Local Import -->


const app = new Vue({
el : '#app',
data : {
currentIndex : 0,

items : [
'Banana',
'Pineapple',
'Apple'
]

},
})


Example:

<!-- Container  -->

- {{item.title}}  Delete



<!-- Local Import -->


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

<!-- Container  -->


Username:
Password:
Login
Reset



<!-- Local Import -->


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

<!-- Container  -->



v-bind with object syntax for CSS


v-bind with array syntax for CSS



<!-- Local Import -->


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:



<!--
Prerequisites:
1. name: Adding name attribute to radio buttons groups them → same group is mutually exclusive
2. value: Adding value attribute to radio buttons for data submission
Combined with Vue → v-model
-->
Gender:
Male
Female


<!--
Prerequisites:
1. option needs value for submission
2. select's value correlates with selected option's value
Combined with Vue → v-model
-->
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 |

<!-- Goal: Sum all quantities -->
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:








Document




| ID | Subject | Score | Action |
|---|---|---|---|
| {{index + 1}} | {{item.subject}} | {{item.score}} | [Delete](https://www.baidu.com/index.htm) |
| 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;
}




<!-- Language selector -->

Translate to:

Italian
English
German



<!-- Translation boxes -->




⌨️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



<!-- Banner -->
![](http://autumnfish.cn/static/fruit.jpg)
<!-- Breadcrumb -->

🏠
/
Cart

<!-- Cart main -->
0 ">

<!-- Header -->


Select
Image
Price
Quantity
Subtotal
Action


<!-- Body -->



![]()
{{item.price}}


-
{{item.quantity}}
+


{{item.price * item.quantity}}
Delete




<!-- Footer -->

<!-- Select all -->


Select All


<!-- Total price -->
Total &nbsp;&nbsp;:&nbsp;&nbsp;¥&nbsp;{{totalAmount}}
<!-- Checkout -->
Checkout( {{selectedCount}} )



<!-- Empty cart -->
🛒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
<!-- CSS only -->


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





<!-- Left list -->


<!-- Add entry -->



Add Bill


| ID | Expense Name | Amount | Action |
|---|---|---|---|
| {{ index + 1 }} | {{ item.name }} | {{ item.price.toFixed(2) }} | [Delete](javascript:;) |
| Total: {{ totalAmount.toFixed(2) }} |


<!-- Right chart -->







/**
* 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



<!-- Header -->

<!-- Main -->

<!-- Footer -->





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

  1. In parent component, pass value via attribute on child component tag
  2. In child component, receive via props
  3. Use in child component template
Implementation
App.vue root component


<!-- 1. Pass value via attribute -->





import Child from './components/Child.vue'

export default {
name: 'App',

data() {
return {
parentTitle : 'Component Communication: Parent to Child'
}
},
components: {
Child : Child
}
}







Child.vue component


<!-- 3. Use props value directly -->
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}}
<!--  1. Bind function, trigger via function -->
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


<!-- 3. Bind function to receive, event is custom name from child not click-type -->





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


<!-- 3. Bind function to receive -->






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 past value
validator(val) {
if (val >= 100 || val <= 0) {
// Custom error message
console.error('Value must be between 0-100')
// return false means validation failed
return false
} else {
// return true means validation passed
return true
}
},
},
},
methods: {
updateWidth() {
this.$emit("updateWidth",30)
}
}
}



.progress-bar {
height: 26px;
width: 400px;
border-radius: 15px;
background-color: #272425;
border: 3px solid #272425;
box-sizing: border-box;
margin-bottom: 30px;
}
.inner {
position: relative;
background: #379bff;
border-radius: 15px;
height: 25px;
box-sizing: border-box;
left: -3px;
top: -2px;
}
.inner span {
position: absolute;
right: 0;
top: 26px;
}


Difference Between props and data
App.vue parent component code







import Counter from './components/Counter.vue'
export default {
components:{
Counter
},
data(){
return {
currentCount:100
}
},
methods:{
handleCountChange(newVal){
this.currentCount = newVal
}
}
}






Counter.vue child component code


-
{{ count }}
+




export default {
// 1. Own data can be modified freely (who owns the data, who manages it)
// data () {
//   return {
//     count: 100,
//   }
// },
// 2. External props cannot be modified directly
props: {
count: {
type: Number,
},
},
methods: {
decrease() {
// count cannot increment, because count belongs to parent
this.$emit('updateCount', this.count - 1)
},
increase() {
this.$emit('updateCount', this.count + 1)
},
},
}



.counter {
margin: 20px;
}


Complete Example

app.vue parent component


<!-- Main area -->








import TodoInput from './components/TodoInput.vue'
import TodoList from './components/TodoList.vue'
import TodoFooter from './components/TodoFooter.vue'

// Rendering:
// 1. Provide data: in shared parent App.vue
// 2. Pass to TodoList via parent-to-child
// 3. Render with v-for

// Add feature:
// 1. Collect form data v-model
// 2. Listen to events (enter + click add)
// 3. Child to parent, pass task name to App.vue
// 4. Add with unshift (own data, own management)
// 5. Clear input
// 6. Validate empty input

// Delete feature
// 1. Listen to delete click with id
// 2. Child to parent, pass delete id to App.vue
// 3. Delete with filter (own data, own management)

// Footer summary: parent to child pass list for rendering
// Clear feature: child to parent notify parent to update
// Persistence: watch deep monitor list changes -> 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


<!-- Input -->

Notes App

Add Task




export default {
data(){
return {
taskName:''
}
},
methods:{
handleAdd(){
this.$emit('add',this.taskName)
this.taskName = ''
}
}
}






TodoList.vue


<!-- List area -->

-  {{ index + 1 }}. {{ item.name }}




export default {
props: {
todos: {
type: Array,
},
},
methods: {
handleDelete(id) {
this.$emit('delete', id)
},
},
}





TodoFooter.vue


<!-- Summary and clear -->

<!-- Summary -->
Total: {{ todos.length }}
<!-- Clear -->
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



<!-- Parent passes data to child -->





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



<!-- 1. When child has props value and emits input event -->
<!-- Can use v-model in parent to simplify -->





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



<!-- Access DOM element -->





import ChartComponent from './components/ChartComponent.vue'
export default {
components:{
ChartComponent
}
}



.chart-box {
width: 200px;
height: 100px;
}


ChartComponent.vue


<!-- ref="customName" -->
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 component instance, can call component methods. Instance available after mounted -->

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 trigggers 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



<!-- Custom v-loading directive -->

- {{ 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



<!-- 2. When using component, fill content inside tags -->

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
✖️



<!-- 1. Place slot where customization is needed -->
<!--
Confirm exit?
content renders here -->



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



<!-- 2. When using component, fill content -->



Confirm exit?





import ModalDialog from './components/ModalDialog.vue'
export default {
data () {
return {

}
},
components: {
ModalDialog
}
}



body {
background-color: #b3b3b3;
}


ModalDialog.vue




Notice
✖️



<!-- 1. Place slot where customization is needed -->
<!-- 2. Fallback content: shows default when no content provided -->
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



<!-- v-bind passes data, list data passed to MyTable.vue via props -->

<!-- 3. Receive via template #slotName="variable", default slot is 'default', obj is object -->


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



&lt; 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

  1. Create new Vue project

vue create project-name

  1. Select Manually select features
  2. Select features
  3. Choose Vue version
  4. Router mode
  • Use history mode for router?
  1. Choose CSS pre-processor
  2. Choose ESLint config
  3. Pick lint features
  4. Where to place config
  5. Save preset

5.7 ESLint Code Standards

Auto-fix Configuration

In settings

  1. Vuex

Related Articles

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

SBUS Signal Analysis and Communication Implementation Using STM32 with Fus Remote Controller

Overview In a recent project, I utilized the SBUS protocol with the Fus remote controller to control a vehicle's basic operations, including movement, lights, and mode switching. This article is aimed...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.