Vue (4) - 重要的元件概念

Vue 的每一個元件都可以獨立儲存各自的狀態,以下是一個元件的範例,此範例以反引號來定義元件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
// 請在此撰寫 JavaScript
Vue.component('counter-component', {
data: function () {
return {
counter: 0
}
},
template: `<div>
<button class="btn btn-outline-secondary btn-sm" @click="counter += 1">{{ counter }}</button>
</div>`
});


var app = new Vue({
el: '#app',
data: {
counter: 0
},
});
</script>

元件可以重複使用,而且帶有一個名字,在這個例子中是 counter-component,我們可以在一個 new Vue 建立的 Vue 應用程式使用此組件:

1
2
3
4
5
6
7
8
9
10
 <div id="app">
<div>
你已經點擊 <counter-component></counter-component> 下。
你已經點擊 <button class="btn btn-outline-secondary btn-sm" @click="counter += 1">{{ counter }}</button> 下。

<counter-component></counter-component>
<counter-component></counter-component>
</div>

</div>

元件是可以重複使用的,它們與 new Vue 接收相同的選項,例如 data、computed、watch、methods 以及生命周期。

template 標籤與 v-if

在希望使用 Vue 指令,但不希望輸出標籤的時候,就可以使用 template 標籤。範例使用 template 標籤搭配 v-if 指令切換多個 DOM 的呈現

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<table class="table">
<thead>
<th>編號</th>
<th>姓名</th>
</thead>
<template v-if="showTemplate">
<tr>
<td>1</td>
<td>安妮</td>
</tr>
<tr v-if="showTemplate">
<td>2</td>
<td>小明</td>
</tr>
</template>

</table>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 <script>
var app = new Vue({
el: '#app',
data: {
isSuccess: true,
showTemplate: true,

link: 'a',

loginType: 'username'
},
methods: {
toggleLoginType: function () {
return this.loginType = this.loginType === 'username' ? 'email' : 'username'
}
}
});
</script>

templete 與 v-for

有兩個 tr 一組使用 v-for,這時就可以運用 templete

1
2
3
4
5
6
7
8
9
10
11
12
<table class="table">
<template v-for="item in arrayData">
<tr>
<td>{{item.age}}</td>
</tr>
<tr>
<td>{{item.name}}</td>
</tr>

</template>

</table>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Vue.component('list-item', {
template: `
<li>
{{ item.name }} {{ item.age }} 歲
</li>
`,
props: ['item']
});

var app = new Vue({
el: '#app',
data: {
arrayData: [{
name: '小明',
age: 16
},
{
name: '漂亮阿姨',
age: 24
},
{
name: '杰倫',
age: 20
}
],
objectData: {
ming: {
name: '小明',
age: 16
},
auntie: {
name: '漂亮阿姨',
age: 24
},
jay: {
name: '杰倫',
age: 20
}
},
filterArray: [],
filterText: ''
}

元件基礎練習 (把表格包裝成元件)

使用 x-templete 建立元件

  • Vue.component 建立元件
  • x-templete 定義元件,使用方式是在外面再建立一個 script ,它的 type 是text/x-templete,id 是指向 templete 的 id

這裡要注意元件內資料與外層是不同的,所以必須將資料傳入元件內

  • 運用 props 傳遞資料
    另外,tbody 裡面不能放非表格相關的標籤,否則會跑版,這樣寫不行
  • 將元件註冊在應用程式下面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="app">
<table class="table">
<thead>
</thead>
<tbody>

<!-- <row-component v-for="(item, key) in data" :person="item" :key="key"></row-component> -->

<tr is="row-component" v-for="(item, key) in data" :person="item" :key="key"></tr>


<!-- <tr v-for="(item, key) in data" :item="item" :key="key">
<td>{{ item.name }}</td>
<td>{{ item.cash }}</td>
<td>{{ item.icash }}</td>
</tr> -->
</tbody>
</table>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
< <script type="text/x-template" id="rowComponentTemplate">
<tr >
<td>{{ person.name }}</td>
<td>{{ person.cash }}</td>
<td>{{ person.icash }}</td>
</tr>
</script>


<script>
// Vue.component('row-component', {
// props: ['person'],
// template: "#rowComponentTemplate"
// })

var child = {
props: ['person'],
template: "#rowComponentTemplate"
}


var app = new Vue({
el: '#app',
data: {
data: [{
name: '小明',
cash: 100,
icash: 500,
},
{
name: '杰倫',
cash: 10000,
icash: 5000,
},
{
name: '漂亮阿姨',
cash: 500,
icash: 500,
},
{
name: '老媽',
cash: 10000,
icash: 100,
},
]
},
components: {
"row-component": child
}
});
</script>

元件必須使用 function return

新建一個 Vue 的應用程式,是不需要 return ,直接用 data 插入物件就可以。但是在元件內如果也用這種方式就會報錯。

1
2
3
4
5
<div id="app">
<counter-component></counter-component>
<counter-component></counter-component>
<counter-component></counter-component>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script type="text/x-template" id="counter-component">
<div>
你已經點擊 <button class="btn btn-outline-secondary btn-sm" @click="counter += 1">{{ counter }}</button> 下。
</div>

</script>

<script>
Vue.component('counter-component', {
data: {
counter: 0
},
template: '#counter-component'
})

var app = new Vue({
el: '#app',
});
</script>

這裡告訴我們需要用 function return

1
2
3
4
5
<div id="app">
<counter-component></counter-component>
<counter-component></counter-component>
<counter-component></counter-component>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script type="text/x-template" id="counter-component">
<div>
你已經點擊 <button class="btn btn-outline-secondary btn-sm" @click="counter += 1">{{ counter }}</button> 下。
</div>

</script>

<script>
Vue.component('counter-component', {
data:function() {
return{counter: 0
},
template: '#counter-component'
})

var app = new Vue({
el: '#app',
});
</script>

props 基本觀念 由外到內的資料傳遞

由於 Vue 的每一個元件資料狀態都是獨立的,因為使用元件的目的就是希望互相干擾的狀況能越小越好,因此資料都是獨立儲存的,除非必要否則不會互相傳遞溝通。如果真的有需要從父層傳遞資料給子層,使用 props 即可。

props down

在 HTML 模版中,使用屬性「user_name」傳遞名稱(name)「Peter」給元件。由於屬性 user_name 的值是由 data 的 name 代入,需要與 Vue Instance 結合,未來在解析模版時做處理,因此加上 v-bind 屬性綁定,簡寫為:。而在元件中,必須使用 props 聲明它所獲得的資料。

1
2
3
<div id="app">
<prompt-component :user-name="name"></prompt-component>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vue.component('prompt-component', {
template: '<button @click="sayHi(userName)">Say Hi!</button>',
delimiters: ['${', '}'],
props: ['user-name'], //使用`props`聲明它所獲得的資料
methods: {
sayHi: function(name) {
alert('Hi ' + name);
}
}
})

var vm = new Vue({
el: '#app',
delimiters: ['${', '}'],
data: {
name: 'Peter'
}
});

camelCase vs kebab-case

HTML 的屬性名稱是大小寫不敏感的,而 JavaScript 是嚴格區分大小寫的。因此,若非使用以 JavaScript 產生模版的方式,意即「字串模版 (String Template)」,而是使用 HTML 模版時,屬性名稱必須使用以 dash (短橫線) 分隔的 kebab-case 命名。

例如,在 HTML 中撰寫屬性名稱「user name」如下

1
2
3
(O) <prompt-component :user-name="name"></prompt-component>

(X) <prompt-component :userName="name"></prompt-component>

重申一次,如果使用字串模板,那就沒有這個限制。

靜態傳遞與動態傳遞

由於屬性 id 的值是由 data 的 id 代入,若只是經由屬性傳遞資料,模版不會做任何處理,得到的資料型態是「string」;但若使用 v-bind 屬性綁定(簡寫為:),將來會與 Vue Instance 結合,解析模版會當成 JavaScript 表達式做計算

  • 靜態傳遞:透過字串直接將資料傳入
  • 動態傳遞:類似 v-bind 的方式在屬性前加冒號

如下範例,元件會代入 id,點擊按鈕後會觸發 checkID method,然後 console 目前 id 的資料型別。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vue.component('prompt-component', {
template: '<button @click="checkID(id)">Check ID</button>',
delimiters: ['${', '}'],
props: ['id'],
methods: {
checkID: function(id) {
console.log(typeof id);
}
}
})

var vm = new Vue({
el: '#app',
delimiters: ['${', '}'],
data: {
id: 1
}
});
  • case 1 靜態
    這裡的 id 的資料型態是「string」。點擊按鈕後 console 出來的結果是 string。
1
<prompt-component id="id"></prompt-component>
  • case 2 動態
    這裡的 id 的資料型態是「number」。點擊按鈕後 console 出來的結果是 number。
1
<prompt-component :id="id"></prompt-component>

單向數據流

Prop 是單向的,只會從父層傳至子層,並且 Prop 的值會隨父層更動設定而改變。

以下的 url 是從外層所傳入的,如果我直接在顯示在畫面上的網址修改

就會報錯,告訴我們不要直接修改 props 傳入的內容

解決方式是必須在元件內新增一個 data 接收外部傳入的 props ,並定義一個新的 url ,不要讓它與外層的 url 綁定。

因此在使用 props 的時候要維持單向數據流的觀念

1
<photo :img-url="url"></photo>
1
2
3
4
5
6
<script type="text/x-template" id="photo">
<div>
<img :src="imgUrl" class="img-fluid" alt="" />
<input type="text" class="form-control" v-model="newUrl"> //將元件由原先綁定 imgUrl 改成綁定 newUrl
</div>
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Vue.component('photo', {
props: ['imgUrl'],
template: '#photo',

// 解答:
// data: function () {
// return {
// newUrl: this.imgUrl
// }
// }
})

var app = new Vue({
el: '#app',
data: {
user: {},
url: 'https://images.unsplash.com/photo-1522204538344-922f76ecc041?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=50e38600a12d623a878983fc5524423f&auto=format&fit=crop&w=1351&q=80',
isShow: true
}
});

尚未宣告的變數

現有一個 card 元件,它會接收外部傳進的 userData,並將卡片繪製出來。userData 的來源是 Vue 應用程式。應用程式裡會先宣告一個 user 物件,再透過 ajax 行為抓取遠端資料,最後才存進 user 裡面。所以 user 傳入就會產生時間差。因此會跳出這個錯誤,它找不到某些變數

簡單來說:如果資料匯入有時間差,我們可以在元件上面使用 v-if 解決這個問題,設定某個特性如果沒有完全讀進來之前先不要執行這個卡片,讓它與資料完成的時候一起同步繪製

1
2
3
4
5
 <div class="row">
<div class="col-sm-4">
<card :user-data="user" v-if="user.phone"></card>
</div>
</div>
1
2
3
4
5
6
7
8
9
Vue.component('card', {
props: ['userData'],
template: '#card',
data: function () {
return {
user: this.userData
}
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vue.component('keepCard', {
template: '#card',
data: function () {
return {
user: {}
}
},
created: function () {
var vm = this;
$.ajax({
url: 'https://randomuser.me/api/',
dataType: 'json',
success: function (data) {
vm.user = data.results[0];
}
});
}
});

維持狀態與生命週期

這個元件每次銷燬再生成的時候,會執行一段 Ajax。如果符合預期的狀態,這樣執行並沒有錯誤。可是有時我們有時不希望元件重新生成的時候,就重頭執行一次 Ajax。

這時使用 keep-alive 維持元件的生命週期

1
2
3
4
5
6
7
8
9
10
11
<h2 class="mt-3">維持狀態與生命週期</h2>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="isShow" v-model="isShow">
<label class="form-check-label" for="isShow">Check me out</label>
</div>
<div class="row">
<div class="col-sm-4" v-if="isShow">
<keep-card>
</keep-card>
</div>
</div>

也要確保在資料載入完成之後才開始執行,所以使用兩個 v-if 的判斷

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</script>


<script type="text/x-template" id="card">
<div class="card">
<img class="card-img-top" :src="user.picture.large" v-if="user.picture" alt="Card image cap">
<div class="card-body">
<h5 class="card-title" v-if="user.name">{{ user.name.first }} {{ user.name.last }}</h5>
<p class="card-text">{{ user.email }}</p>
</div>
<div class="card-footer">
<input type="email" class="form-control" v-model="user.email">
</div>
</div>
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Vue.component('keepCard', {
template: '#card',
data: function () {
return {
user: {}
}
},
created: function () {
var vm = this;
$.ajax({
url: 'https://randomuser.me/api/',
dataType: 'json',
success: function (data) {
vm.user = data.results[0];
}
});
}
});

var app = new Vue({
el: '#app',
data: {
user: {},
url: 'https://images.unsplash.com/photo-1522204538344-922f76ecc041?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=50e38600a12d623a878983fc5524423f&auto=format&fit=crop&w=1351&q=80',
isShow: true
},
created: function () {
var vm = this;
$.ajax({
url: 'https://randomuser.me/api/',
dataType: 'json',
success: function (data) {
vm.user = data.results[0];
}
});
}
});

props 型別及預設值

在傳入 props 的時候就定義型別,避免傳入錯誤內容

1
2
3
4
5
6
7
8
9
<div id="app">
<h2>Props 的型別</h2>
<prop-type :cash="cash"></prop-type>

<h2 class="mt-3">靜態與動態傳入數值差異</h2>
<!-- 有設定type的話,靜態傳入參數就會錯誤,要改為動態傳入參數 -->
<!-- :cash 有冒號,是動態傳入參數,沒有冒號是靜態傳入參數 -->
<prop-type :cash="300"></prop-type>
</div>
1
2
3
4
5
<script type="text/x-template" id="propType">
<div>
<input type="number" class="form-control" v-model="newCash"> {{ typeof(cash)}}
</div>
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script>
Vue.component('prop-type', {
//props: ['cash'],
//改為以下的寫法,props改設定為物件
//物件內可設定變數的屬性以及預設值
//有設定型態的好處就是傳入字串的時候就會出現錯誤
props: {
cash: {
type: Number,
default: 100
}
},
template: '#propType',
data: function() {
return {
newCash: this.cash
}
}
});


var app = new Vue({
el: '#app',
data: {
cash: 300
}
});
</script>

emit 基本觀念 由內向外傳遞資訊

現在要透過裡面的元件改變外面 cash 金額

  • 要觸發 Vue 應用程式,所以要有一個按鈕點擊事件 incrementTotal
  • 再註冊另一個事件,名稱可以自定稱為increment,並啟用上面的 incrementTotal
  • 現在內部元件 click 事件有一個 incrementCounter,使用 emit 讓它可以觸發外層 increment 的實體事件
1
2
3
4
5
6
<div id="app">
<h2>透過 emit 向外傳遞資訊</h2>
我透過元件儲值了 {{ cash }} 元
<button class="btn btn-primary" v-on:click="incrementTotal">點我</button>
<button-counter v-on:increment="incrementTotal"></button-counter>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script>
Vue.component('buttonCounter', {
template: `<div>
<button @click="incrementCounter" class="btn btn-outline-primary" >增加 {{ counter }} 元</button>
<input type="number" class="form-control mt-2" v-model="counter">
</div>`,
data: function () {
return {
counter: 1
}
},
methods: {
incrementCounter: function () {
this.$emit('increment', Number(this.counter));
}
}
});

var app = new Vue({
el: '#app',
data: {
cash: 300
},
methods: {
incrementTotal: function (newNumber) {
this.cash = this.cash + newNumber;
}
}
});
</script>

Slot 插槽替換

有時候重複使用的元件需要替換部分內容,所以需要替換一些模版

  • 沒有插槽可以替換的狀態

    插入的內容完全都會被模版替換,該內容不是模版的一部分所以無法顯示
1
2
3
4
5
6
<div id="app">
<h2>沒有插槽可替換的狀態</h2>
<no-slot-component>
<p>這是一段插入的內容喔</p>
</no-slot-component>
</div>
  • Slot 基礎範例
    現在要把 <p>使用這段取代原本的 Slot。</p> 放進元件內。所以我們在元件內新增一個 slot 標籤
1
2
3
4
5
<div id="app">
<single-slot-component>
<p>使用這段取代原本的 Slot。</p>
</single-slot-component>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
</script>

<script type="text/x-template" id="singleSlotComponent">
<div class="alert alert-warning">
<h6>我是一個元件</h6>
<div>
如果沒有內容,則會顯示此段落。
</div>

<slot></slot>
</div>
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
Vue.component('no-slot-component', {
template: '#noSlotComponent',
});

Vue.component('single-slot-component', {
template: '#singleSlotComponent',
});

Vue.component('named-slot-component', {
template: '#namedSlotComponent',
});

var app = new Vue({
el: '#app',
data: {}
});
</script>

  • 具名插槽
    有時有大量內容需要取代而且分布在元件不同地方。我們在 templeteslot 標籤加上自定義的名稱做對應。並在實體上加上相同的屬性
1
2
3
4
5
6
<named-slot-component>
<header slot="header">替換的 Header</header>
<template>替換的 Footer</template>
<template slot="btn">按鈕內容</template>
<p>其餘的內容</p>
</named-slot-component>

附註:如果不希望標籤像這樣輸出的話,就使用 templete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

<script type="text/x-template" id="namedSlotComponent">
<div class="card my-3">
<div class="card-header">
<slot name="header">這段是預設的文字</slot>
</div>
<div class="card-body">
<slot>
<h5 class="card-title">Special title treatment</h5>
<p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
</slot>
<a href="#" class="btn btn-primary">
<slot name="btn">spanGo somewhere</slot>
</a>
</div>
<div class="card-footer">
<div>這是預設的 Footer</div>
</div>
</div>
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
Vue.component('no-slot-component', {
template: '#noSlotComponent',
});

Vue.component('single-slot-component', {
template: '#singleSlotComponent',
});

Vue.component('named-slot-component', {
template: '#namedSlotComponent',
});

var app = new Vue({
el: '#app',
data: {}
});
</script>

使用 is 動態切換組件

在 Vue 應用程式的 current 變數裡的 primary-component,切換成 danger-component,並透過 :is 動態切換元件內容

1
2
3
4
5
6
<div class="mt-3">
<!-- <primary-component :data="item" v-if="current === 'primary-component'"></primary-component>
<danger-component :data="item" v-if="current === 'danger-component'"></danger-component> -->
<div :is="current" :data="item"></div>

</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
Vue.component('primary-component', {
props: ['data'],
template: '#primaryComponent',
});
Vue.component('danger-component', {
props: ['data'],
template: '#dangerComponent',
});

var app = new Vue({
el: '#app',
data: {
item: {
header: '這裡是 header',
title: '這裡是 title',
text: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim perferendis illo reprehenderit ex natus earum explicabo modi voluptas cupiditate aperiam, quasi quisquam mollitia velit ut odio vitae atque incidunt minus?'
},
current: 'primary-component'
}
});
</script>

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2020 CYC'S BLOG All Rights Reserved.

UV : | PV :