angular學習(十三)——Component
理解Components
在AngularJS中,Component是一種特殊的directive,它的配置更簡單一些,非常適合元件化的app架構。使用web元件和使用Angular風格的app架構使得編寫app更為簡便。
Component的優點:
- 比普通directive要簡單很多
- 更加嚴謹,更加規範化
- 更加適合元件化架構
- component更容易升級到angular2
不使用Component的情況
- 需要在編譯階段和預連結階段執行的directive,因為Component這時還不可用
- 當你需要directive才有定義的選項時,如priority, terminal, multi-element
- 當你需要directive由屬性,css的class而不是元素觸發時
Components的建立和配置
Components由angularjs的module使用.component()
方法註冊。這個方法接受2個引數:
- Component的名稱(字串型別)
- Component的配置物件(注意,和
.directive()
不一樣,不是一個工廠方法而僅僅是個配置物件)
heroApp.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="uft-8"/>
<title></title >
</head>
<script src="script/angular.min.js"></script>
<body ng-app="heroApp">
<div ng-controller="MainCtrl as ctrl">
<b>Hero</b><br>
<hero-detail hero="ctrl.hero"></hero-detail>
</div>
</body>
<script>
angular.module('heroApp' , []).controller('MainCtrl', function MainCtrl() {
this.hero = {
name: 'Spawn'
};
});
function HeroDetailController() {
}
angular.module('heroApp').component('heroDetail', {
templateUrl: 'heroDetail.html',
controller: HeroDetailController,
bindings: {
hero: '='
}
});
</script>
</html>
- 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
- 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
heroDetail.html:
<span>Name: {{$ctrl.hero.name}}</span>
- 1
- 1
Directive和Component之間的定義比較
屬性 | Directive | Component |
---|---|---|
bindings | No | Yes (binds to controller) |
bindToController | Yes (default: false) | No (use bindings instead) |
compile function | Yes | NO |
controller | Yes | Yes (default function() {}) |
controllerAs | Yes (default: false) |
Yes (default: $ctrl ) |
link functions | Yes | No |
multiElement | Yes | No |
priority | Yes | No |
replace | Yes (deprecated) | No |
require | Yes | Yes |
restrict | Yes | No (restricted to elements only) |
scope | Yes (default: false) | No (scope is always isolate) |
template | Yes | Yes, injectable |
templateNamespace | Yes | No |
templateUrl | Yes | Yes, injectable |
terminal | Yes | No |
transclude | Yes (default: false) | Yes (default: false) |
元件化app架構
如前所述,component使得使用元件化架構構建app更為容易,除此之外component還具備的能力具體如下:
- Component只能控制它自己的檢視和資料:Component不會修改它自身scope之外的任何資料或DOM。通常情況下,AngularJS中可以通過scope繼承和watch可以修改任何地方的資料,這確實很實用,但是也會導致很難明確哪個部分對修改資料負責。這就是為什麼Component使用隔離範圍,也因此無法進行所有scope的操作。
-
Component有明確定義的公共api-輸入輸出:隔離範圍並不是全部,因為AngularJS是雙向繫結的。如果你傳一個物件到元件中,類似
bindings: {item: '='}
,然後修改物件的屬性,修改會反映到它的父元件中。但是對於component來說,component確實只是修改了它自己的scope內的資料。這樣就很清晰的得知什麼資料什麼時候被修改。就此,component遵循一些簡單的約定:-
輸入需要使用
<
和@
繫結。<
符號在1.5之後表示單向繫結,和=
不同的是,它繫結的屬性在component的scope哪不會被watch,這意味著你可以在component的scope內給屬性設定一個新的值,它並不會更新父scope裡的值。但是如果父scope和component的scope引用的是同一個物件,比如你在component修改物件的屬性或者陣列中的元素,這種改變仍然會反映到父scope。因此在<
繫結後不要在component內修改物件的屬性和陣列的元素。當輸入的值是字串時可以使用@
繫結,特別是在繫結的值不會更改的時候。bindings: { hero: '<', comment: '@' }
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
-
輸出使用
&
進行繫結,繫結的函式將作為component事件的回撥函式bindings: { onDelete: '&', onUpdate: '&' }
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
-
在不操作輸入資料的情況下,component可以呼叫相應的輸出事件改變輸出資料。比如刪除,並不是hero自己刪除自己,而是通過相應的事件把自己傳給父component
<!-- note that we use kebab-case for bindings in the template as usual --> <editable-field on-update="$ctrl.update('location', value)"></editable-field><br> <button ng-click="$ctrl.onDelete({hero: $ctrl.hero})">Delete</button>
- 1
- 2
- 3
- 1
- 2
- 3
-
這樣,父component來決定事件執行什麼(刪除item還是更新屬性)
ctrl.deleteHero(hero) { $http.delete(...).then(function() { var idx = ctrl.list.indexOf(hero); if (idx >= 0) { ctrl.list.splice(idx, 1); } }); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
-
-
Component有明確定義的生命週期:每個元件都可以實現
lifecycle hooks
,這些方法會在component生命週期的相應的點被呼叫,可以實現的hook方法如下:$onInit()
- 在element上的所有controller構造和所有的繫結初始化之後,在element之上字首&的函式連結之前,每一個controller呼叫這個鉤子方法。這是個給controller寫一些初始化方法的好地方$onChanges(changesObj)
- 當單向繫結更新時呼叫,changesObj是一個hash鍵值對,key是已被修改的繫結屬性的name,value是一個物件,格式是{ currentValue, previousValue, isFirstChange() }
,使用這個hook可以只觸發元件內的更新,比如克隆繫結屬性的物件以防止它被外部意外更新$doCheck()
- 在每一個digest迴圈被呼叫,提供了一個機會可以在資料更改時驗證或者做一些操作。任何你希望進行的操作(比如對更改的響應)都必須通過這個hook呼叫。當$onChanges
被呼叫時,實現這個hook沒什麼作用。比如,比如你想實現一個深度的equal函式檢查,或者檢查一個Date物件時,他將會非常有用,因為這是AngularJS的變化檢測器檢測不到這個變化,自然$onChanges
也不會被呼叫。這個hook不包含任何引數,因此,如果想要檢測變化,你需要儲存之前的值,然後和現在的值進行比較。$onDestroy()
- 當controller包含的scope銷燬時,controller會呼叫這個hook。使用這個hook可以釋放一些外部資源,watch和事件處理程式$postLink()
- 在controller的element和子元素被連結後呼叫。和post-link函式類似,這個hook可以設定DOM事件的處理程式或者直接進行DOM操作。包含templateUrl指令的element不會被編譯和連結,因為它們要等template非同步載入。它們的編譯和連結要等template完成之後才會執行。這個hook非常和angular2中的ngAfterViewInit和ngAfterContentInit兩個hook類似。因為angular1和angular2編譯過程的不同,所以在anguar1升級到angular2時一定要小心。
通過實現這些方法,你的component可以在生命週期中hook
-
app就是一棵component樹:理想的情況下,整個app是一棵component構成的樹,清晰的定義了輸入輸出,並儘量不使用雙向繫結。這樣更容易預測資料的變化和元件狀態。
Component樹示例
下面這個示例對上面對例子進行了擴充套件,並和我們剛講過到概念結合起來:
不再使用ngController,現在使用擁有多個hero的heroList元件象,為每個hero建立一個heroDetail元件。
heroDetail元件包含的新功能:
- 一個delete按鈕,可以呼叫繫結到heroList元件的onDelete函式
- 一個可以改變hero的location的輸入控制元件,該控制元件是可重用的editableField元件。不是editableField元件自己操作hero物件,它將改變傳給heroDetail元件,heroDetail元件再傳給heroList元件,由heroList元件更新原始資料。
heroApp.html
<!DOCTYPE html>
<html>
<head>
<meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="heroApp">
<hero-list></hero-list>
</body>
<script>
/**
* index.js
*/
angular.module('heroApp', []);
/**
* heroList.js
*/
function HeroListController($scope, $element, $attrs) {
var ctrl = this;
// This would be loaded by $http etc.
ctrl.list = [
{
name: 'Superman',
location: ''
},
{
name: 'Batman',
location: 'Wayne Manor'
}
];
ctrl.updateHero = function(hero, prop, value) {
hero[prop] = value;
};
ctrl.deleteHero = function(hero) {
var idx = ctrl.list.indexOf(hero);
if (idx >= 0) {
ctrl.list.splice(idx, 1);
}
};
}
angular.module('heroApp').component('heroList', {
templateUrl: 'heroList.html',
controller: HeroListController
});
/**
* heroDetail.js
*/
function HeroDetailController() {
var ctrl = this;
ctrl.delete = function() {
ctrl.onDelete({hero: ctrl.hero});
};
ctrl.update = function(prop, value) {
ctrl.onUpdate({hero: ctrl.hero, prop: prop, value: value});
};
}
angular.module('heroApp').component('heroDetail', {
templateUrl: 'heroDetail.html',
controller: HeroDetailController,
bindings: {
hero: '<',
onDelete: '&',
onUpdate: '&'
}
});
/**
* editableField.js
*/
function EditableFieldController($scope, $element, $attrs) {
var ctrl = this;
ctrl.editMode = false;
ctrl.handleModeChange = function() {
if (ctrl.editMode) {
ctrl.onUpdate({value: ctrl.fieldValue});
ctrl.fieldValueCopy = ctrl.fieldValue;
}
ctrl.editMode = !ctrl.editMode;
};
ctrl.reset = function() {
ctrl.fieldValue = ctrl.fieldValueCopy;
};
ctrl.$onInit = function() {
// Make a copy of the initial value to be able to reset it later
ctrl.fieldValueCopy = ctrl.fieldValue;
// Set a default fieldType
if (!ctrl.fieldType) {
ctrl.fieldType = 'text';
}
};
}
angular.module('heroApp').component('editableField', {
templateUrl: 'editableField.html',
controller: EditableFieldController,
bindings: {
fieldValue: '<',
fieldType: '@?',
onUpdate: '&'
}
});
</script>
</html>
- 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
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 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
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
heroList.html
<b>Heroes</b><br>
<hero-detail ng-repeat="hero in $ctrl.list" hero="hero" on-delete="$ctrl.deleteHero(hero)" on-update="$ctrl.updateHero(hero, prop, value)"></hero-detail>
- 1
- 2
- 1
- 2
heroDetail.html
<hr>
<div>
Name: {{$ctrl.hero.name}}<br>
Location: <editable-field field-value="$ctrl.hero.location" field-type="text" on-update="$ctrl.update('location', value)"></editable-field><br>
<button ng-click="$ctrl.delete()">Delete</button>
</div>
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
editableField.html
<span ng-switch="$ctrl.editMode">
<input ng-switch-when="true" type="{{$ctrl.fieldType}}" ng-model="$ctrl.fieldValue">
<span ng-switch-default>{{$ctrl.fieldValue}}</span>
</span>
<button ng-click="$ctrl.handleModeChange()">{{$ctrl.editMode ? 'Save' : 'Edit'}}</button>
<button ng-if="$ctrl.editMode" ng-click="$ctrl.reset()">Reset</button>
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
Component作為route模版
當使用ngRoute時,Component作為route的模版也是非常有用的。一個元件化的app,每一個檢視都是一個元件:
var myMod = angular.module('myMod', ['ngRoute']);
myMod.component('home', {
template: '<h1>Home</h1><p>Hello, {{ $ctrl.user.name }} !</p>',
controller: function() {
this.user = {name: 'world'};
}
});
myMod.config(function($routeProvider) {
$routeProvider.when('/', {
template: '<home></home>'
});
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
使用$routeProvider
時,你常常可以避開一些樣板將已解決達route依賴傳到component中。1.5版本之後,ngRoute可以自動分配resolve到route的scope屬性$resolve
,你也可以通過resolveAs配置屬性名。當用component時,你可以利用component的優勢,直接把resolve傳到你的component中,而不用建立額外的route控制器。
var myMod = angular.module('myMod', ['ngRoute']);
myMod.component('home', {
template: '<h1>Home</h1><p>Hello, {{ $ctrl.user.name }} !</p>',
bindings: {
user: '<'
}
});
myMod.config(function($routeProvider) {
$routeProvider.when('/', {
template: '<home user="$resolve.user"></home>',
resolve: {
user: function($http) { return $http.get('...'); }
}
});
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
Component間的通訊
Directive需要其他Directive的controller來相互通訊。在component中可以為require屬性提供一個物件map來實現,map的key是所需controller構成的屬性,而map的值是所需元件的名稱。
不過要注意的是,所需的controller在它初始化完成之前是不可用的,但是可以確定的是當前controller在$onInit
執行之前所需的controller是可用的。
下面的例子是將上一篇文章由Directive方式改為了component方式:
docsTabsExample.html
<!DOCTYPE html>
<html>
<head>
<meta charset="uft-8"/>
<title></title>
</head>
<script src="script/angular.min.js"></script>
<body ng-app="docsTabsExample">
<my-tabs>
<my-pane title="Hello">
<p>Lorem ipsum dolor sit amet</p>
</my-pane>
<my-pane title="World">
<em>Mauris elementum elementum enim at suscipit.</em>
<p><a href ng-click="i = i + 1">counter: {{i || 0}}</a></p>
</my-pane>
</my-tabs>
</body>
<script>
angular.module('docsTabsExample', [])
.component('myTabs', {
transclude: true,
controller: ['$scope', function MyTabsController($scope) {
var panes = this.panes = [];
this.select = function (pane) {
angular.forEach(panes, function (pane) {
pane.selected = false;
});
pane.selected = true;
};
this.addPane = function (pane) {
if (panes.length === 0) {
this.select(pane);
}
panes.push(pane);
};
}],
templateUrl: 'my-tabs.html'
})
.component('myPane', {
require: {
tabsCtrl: '^myTabs'
},
transclude: true,
bindings: {
title: '@'
},
controller: function