[course] Learn and Understand AngularJS
此篇為各筆記之整理,非原創內容,主要資料來源為 Learn and Understand AngularJS @ Udemy
Model, View, Whatever
在 HTML 中使用 ng-app
和 ng-controller
標註要被綁定的 DOM:
<html lang="en-us" ng-app="myApp">
<!-- ... -->
<body>
<div ng-controller="mainController">
<!-- ... -->
</div>
</body>
</html>
註冊 App 和 Controller:
// 註冊 App
const myApp = angular.module('myApp', []);
// 註冊 Controller
myApp.controller('mainController', function ($scope) {/* ... */});
Services and Dependency Injection
- services @ developer guide
- service components @ API
Dependency Injection
Dependency Injection 指的是「不在函式中建立一個物件,而是把該物件當成參數傳入函式中,達到物件和函式的解耦」。
在 Angular 中,會利用 fn.toString()
的方式將函式轉成字串後進行解析:
const searchPeople = function (firstName, lastName, height, age, occupation) {
return 'John Doe';
};
console.log(searchPeople.toString());
// "function (firstName, lastName, height, age, occupation) {...}"
console.log(angular.injector().annotate(searchPeople));
// ['firstName', 'lastName', 'height', 'age', 'occupation']
在 App 中添加 services
// 第二個參數的 [] 中課業放入要添加的 service
const myApp = angular.module('myApp', ['ngMessages', 'ngResource']);
// 在 controller 中即可透過 dependency injection 取得
myApp.controller('mainController', function ($scope, $resource) {
/* ... */
});
在 Controller 中取用服務
Angular 利用 dependency 讓開發者在 controller 的 callback function 中可以透過參數取用功能。
angular 的 dependencies injection 會解析 callback function 中參數的「名稱」(例如,$scope
、$log
),而不是看參數的順序,因此名稱一定要正確,參數的順序則不重要:
myApp.controller('mainController', function ($scope, $log, $filter) {
$scope.name = 'Aaron';
$scope.formattedName = $filter('uppercase')($scope.name);
$log.info($scope.name);
$log.info($scope.formattedName);
});
前面提到,參數的名稱是重要的,但在 minify 後,這些參數的名稱都會被調整,這會導致的 minify 後 angular 無法順利執行 dependencies injection。
為了避免這個問題,angular 提供了另一種撰寫的方式,這種方式的話,參數名稱就不重要,而是順序才會是重要的:
myApp.controller('mainController', [
'$scope',
'$log',
'$filter',
function ($scope, $log, $filter) {
$scope.name = 'John';
$scope.formattedName = $filter('uppercase')($scope.name);
$log.info($scope.name);
$log.info($scope.formattedName);
},
]);
Nested Controllers
一個 Controller 是可以被 nested 在另一個 controller 底下,當變數的命名重複時,預設的情況下,AngularJS 會找到最靠近它的 Controller。
$parent
如果 Nested 的 Controlled 需要取用到同變數名稱的父層 controlled 的變數,需要使用 $parent
:
<div ng-controller="parent1Controller">
<!-- Parent 1 Message! -->
{{ message }}
<div ng-controller="child1Controller">
<!-- Child 1 Message! -->
{{ message }}
<!-- Parent 1 Message! -->
{{ $parent.message }}
</div>
</div>
<div ng-controller="parent2Controller">
<!-- Parent 2 Message! -->
{{ message }}
<div ng-controller="child2Controller">
<!-- Child 2 Message! -->
{{ message }}
</div>
</div>
<script>
var myApp = angular.module('myApp', []);
myApp.controller('parent1Controller', [
'$scope',
function ($scope) {
$scope.message = 'Parent 1 Message!';
},
]);
myApp.controller('child1Controller', [
'$scope',
function ($scope) {
$scope.message = 'Child 1 Message!';
},
]);
myApp.controller('parent2Controller', [
'$scope',
function ($scope) {
$scope.message = 'Parent 2 Message!';
},
]);
myApp.controller('child2Controller', [
'$scope',
function ($scope) {
$scope.message = 'Child 2 Message!';
},
]);
</script>
未來避免重複使用 $parent
,有另外兩種方式可以採用。
Controller As
- 在 template 的
ng-controller
內,使用xxxController as xxx
- 在 xxxController 不使用
$scope
service,而是直接使用this
來保存變數 - 如果需要用到 watch 的話,還是需要用
$scope
<!-- 使用 as xxx -->
<div ng-controller="parent2Controller as parent2vm">
<!-- Parent 2 Message! -->
{{ parent2vm.message }}
<div ng-controller="child2Controller as child2vm">
<!-- Child 2 Message! -->
{{ child2vm.message }}
</div>
</div>
<script>
// 不使用 $scope 變數,而是直接使用 this
myApp.controller('parent2Controller', [
function () {
this.message = 'Parent 2 Message!';
},
]);
myApp.controller('child2Controller', [
function () {
this.message = 'Child 2 Message!';
},
]);
</script>
Data Binding and Directives
Scope and Interpolation
$scope
物件內的變數可以傳到 view- 在 view 中可以使用
{{ }}
來做 string interpolation 取得 $scope 內的變數值
<html lang="en-us" ng-app="myApp">
<head>
<script>
const myApp = angular.module('myApp', []);
myApp.controller('mainController', [
'$scope',
'$timeout',
function ($scope, $timeout) {
$scope.name = 'John';
$timeout(function () {
$scope.name = 'Aaron';
}, 2000);
},
]);
</script>
</head>
<body>
<div ng-controller="mainController">
<h1>Hello {{ name }}!</h1>
</div>
</body>
</html>
Two Way Data Binding
<html lang="en-us" ng-app="myApp">
<head>
<script>
const myApp = angular.module('myApp', []);
myApp.controller('mainController', [
'$scope',
'$filter',
function ($scope, $filter) {
$scope.handle = '';
$scope.lowercaseHandle = () => $filter('lowercase')($scope.handle);
},
]);
</script>
</head>
<body>
<div ng-controller="mainController">
<div>
<label for="">What is your twitter handler?</label>
<input type="text" ng-model="handle" />
</div>
<hr />
<h1>twitter.com/{{ lowercaseHandle() }}</h1>
</div>
</body>
</html>
Watchers and the Digest Loop
myApp.controller('mainController', ['$scope'], function ($scope) {
$scope.name = '';
$scope.watch('name', function (newValue, oldValue) {
console.log({ newValue, oldValue });
});
});
Angular Context
需要使用 AngularJS 提供的方法,AngularJS 才會偵測到 $scope
的資料有改變,也才會進一步去更新 DOM:
myApp.controller('mainController', ['$scope'], function ($scope) {
$scope.handle = '';
// 如果用 setTimeout,但沒有把會改變 $scope 的部分包在 $scope.$apply 中,
// AngularJS 不會偵測到資料改變,也不會重新 render DOM
setTimeout(function () {
$scope.$apply(function () {
$scope.handle = 'new-twitter-handle';
});
});
});
AngularJS 提供的方法都會以 $
開頭。
External Data and $http
myApp.controller('mainController', [
'$scope',
'$filter',
'$http',
function ($scope, $filter, $http) {
$http
.get('https://jsonplaceholder.typicode.com/todos/')
.success((result) => {
$scope.todos = result;
})
.error((data, statusCode) => {
console.log({
data,
statusCode,
});
});
},
]);
Common Directives
directive @ angularJS
ng-if, ng-show, and ng-hide
<!-- could use "ng-show", "ng-hide" to see the difference -->
<div class="alert" ng-if="handle.length !== characters">Must be 5 characters!</div>
ng-class
<div
class="alert"
ng-if="handle.length !== characters"
ng-class="{
'alert-warning': handle.length < characters,
'alert-danger': handle.length > characters
}"
>
Must be 5 characters!
</div>
ng-repeat
<script>
myApp.controller('mainController', [
'$scope',
'$filter',
function ($scope, $filter) {
$scope.rules = [
{ ruleName: 'Must be 5 characters' },
{ ruleName: 'Must not be used elsewhere' },
{ ruleName: 'Must be cool' },
];
},
]);
</script>
<ul>
<li ng-repeat="rule in rules">{{ rule.ruleName }}</li>
</ul>
events: ng-click
<script>
const myApp = angular.module('myApp', []);
myApp.controller('mainController', [
'$scope',
function ($scope) {
$scope.alertClick = function () {
alert('clicked');
};
},
]);
</script>
<button type="button" ng-click="alertClick()">Click</button>
ng-transclude
Transclusion 指的是複製一個文件放到另一個文件內,類似 slot 的概念。
在 AngularJS 中,可以透過 ng-transclude
把 element 內(children)的東西,顯示在 <ng-transclude></ng-transclude>
中。
在使用 directive element 的地方:
<search-result person-object="person" get-formatted-address="getFormattedAddress(aPerson)">
*Text here are going to be transcluded
</search-result>
在 directive 的地方開啟 transclude 功能:
myApp.directive('searchResult', function () {
return {
// ...
transclude: true,
};
});
使用 <ng-transclude>
來說明要把被 transclude (children) 的內容放在哪:
<a href="#">
<h4>{{personObject.name}}</h4>
<p>{{getFormattedAddress({aPerson: personObject})}}</p>
<small>
<ng-transclude></ng-transclude>
</small>
<!-- 也可以寫這樣 -->
<small ng-transclude></small>
</a>
MISC
ng-cloak
Big Words
- Directive: An instruction to AngularJS to manipulation a piece of the DOM
Custom Services
Singleton
在 Angular Controller 中,除了 $scope
之外,其他都是 singleton,也就是說,不同 controller 實際上取用到的是同一個物件:
const myApp = angular.module('myApp', []);
myApp.controller('mainController', [
'$scope',
'$log'
function ($scope, $log) {
$scope.main = 'Main Controller';
$log.main = 'Main Controller';
},
]);
myApp.controller('secondController', [
'$scope',
'$log'
function ($scope, $log) {
$scope.second = 'Second Controller';
$log.second = 'Second Controller';
// 因為 $log 是 singleton,所以裡面會包含 "Main" 的東西
$log.log($log);
// $scope 是例外
$log.log($scope);
},
]);
Creating a Service
在 AngularJS 中,要定義和使用 Service 相當容易,但要記得的是,這些 service 也都會是 singleton,也就是不同的 controller 或 page 之間,會共用到的是相同的物件:
// 定義一個 service
myApp.service('nameService', function () {
const self = this;
this.name = 'John Doe';
this.nameLength = function () {
return self.name.length;
};
});
// 在 controller 中可以直接 inject 這個 service
myApp.controller('secondController', [
'$scope',
'nameService',
function ($scope, nameService) {
$scope.name = nameService.name;
$scope.length = nameService.nameLength();
},
]);
service 的資料不會自動 two-way binding
另外,當 scope 的資料改變時,AngularJS 並不會自動更新回 service,因此,有些時候可能需要使用 watch:
myApp.controller('secondController', [
'$scope',
'nameService',
function ($scope, nameService) {
$scope.name = nameService.name;
// 一旦 scope 的 name 改變,就同步回 nameService 中
$scope.$watch('name', function () {
nameService.name = $scope.name;
});
},
]);
在 service 中使用其他 service
有些時候,會需要在 service 中使用其他建立好的 service,一樣可以像在 controller 中使用 injection 來取用,例如下面的 $resource
service:
// 建立 service
myApp.service('weatherService', [
'$resource',
function ($resource) {
this.GetWeather = function (city, days) {
const weatherAPI = $resource('<API_ENDPOINT>');
return weatherAPI.get({ q: city, cnt: days });
};
},
]);
// 使用 service
myApp.controller('forecastController', [
'$scope',
'weatherService',
function ($scope, weatherService) {
weatherService.GetWeather($scope.city, $scope.days);
},
]);
Custom Directives / Reusable Components
Creating Custom Directives @ AngularJS Developer Guide
在 AngularJS 中,使用 custom directives 可以建立複用的元件:
/**
* define custom directive
*/
myApp.directive('searchResult', function () {
return {
// 定義 AngularJS 辨認那些是 custom directive 的方式
restrict: 'AECM', // default 是 AE。分別代表 Attribute, Element, Class, coMment
// 直接定義 HTML 的內容作為 template
// template: '<h4>Doe, John</h4>',
// 載入 HTML 檔案作為 template
templateUrl: 'directives/search-result.html',
// 不要在最終的 HTML 中出現 <search-result> 的元素
replace: true,
};
});
<div class="list-group">
<search-result></search-result>
<!-- Element -->
<div search-result></div>
<!-- Attribute -->
<div class="search-result"></div>
<!-- Class -->
<!-- directive: search-result --><!-- coMment -->
</div>
Scope
Isolated Scope @ AngularJS API > $compile
預設的情況下,custom directive / reusable component 是可以取用到它父層 controller 的所有資料,這樣雖然方便,但同時也有風險。這時候,AngularJS 提供了 isolated scope 的功能,縮限該 directive 能夠操作或取用到的資料範圍。
- Text:
@
- Two-way binding:
=
- One-way binding:
<
- eExecute expression:
&
@ : Text(string)
將資料從 element 的地方傳入:
<search-result
person-name="{{ person.name }}"
person-address="{{ person.address }}"
></search-result>
在 custom directive 的 HTML template 中使用:
myApp.directive('searchResult', function () {
return {
// ...
// isolated scope
scope: {
personName: '@', // 等同於:personName: '@personName',需要的話可以在這裡改名
address: '@personAddress', // 把傳入的 personAddress 改名成 address
},
};
});
<a href="#">
<h4>{{personName}}</h4>
<p>{{address}}</p>
</a>
= : Two-way binding(Object)
將資料從 element 的地方傳入:
<search-result person-object="person"></search-result>
在 custom directive 的 HTML template 中使用:
myApp.directive('searchResult', function () {
return {
// ...
// isolated scope
scope: {
personObject: '=',
},
};
});
<a href="#">
<h4>{{personObject.name}}</h4>
<p>{{personObject.address}}</p>
</a>
& : function (evaluation)
在 controller 定義 function:
myApp.controller('mainController', [
'$scope',
'$log',
function ($scope, $log) {
$scope.person = {
name: 'John Doe',
address: '555 Main St., New York, NY 11111',
city: 'New York',
state: 'NY',
zip: '11111',
};
$scope.getFormattedAddress = function (person) {
return person.address + ', ' + person.city + ', ' + person.state + ' ' + person.zip;
};
},
]);
將資料從 element 的地方傳入:
- 留意傳入的 function
getFormattedAddress
中的參數是可以任意修改的(例如,這裡叫aPerson
),只要在呼叫它的地方要對應到
<search-result
person-object="person"
get-formatted-address="getFormattedAddress(aPerson)"
></search-result>
在 custom directive 的 HTML template 中使用:
myApp.directive('searchResult', function () {
return {
// ...
// isolated scope
scope: {
personObject: '=',
getFormattedAddress: '&',
},
};
});
在 HTML 呼叫這個 getFormattedAddress
的時候,並不是直接把參數帶入,而是要以物件的方式帶入,且物件的 key
名稱要和剛剛傳入時的對應(即,aPerson
):
<a>
<h4>{{personObject.name}}</h4>
<p>
{{getFormattedAddress({ aPerson: personObject })}}
</p>
</a>
Compile and Link
從 Computer Science 的角度來說:
- compiler:透過 compiler 將所寫的程式碼轉換成機器能讀懂的低階語言
- linker:產生電腦實際能執行的檔案
在 AngularJS 中,提供 compile 和 link 的 hook 讓開發者可以在中間做一些介入,其中
compile
:只會執行一次,類似 initialize,可以取得原始的 HTML template。幾乎不會用到。- link:使用到該 template 幾次就會執行幾次
pre
:不應該使用post
:類似 onBind,需要做一些介入的話,可以在這個 hook
myApp.directive('searchResult', function () {
return {
// ...
compile: function (elem, attrs) {
// 拿到 templateUrl 中原始的 HTML 內容
console.log('Compiling...');
console.log(elem.html());
return {
// NOT SAFE: should not use the pre-link hook
pre: function (scope, elements, attrs) {
console.log('Pre-linking...');
},
/**
* scope: modal, data
* elements: view
*/
post: function (scope, elements, attrs) {
console.log('Post-linking...');
if (scope.personObject.name === 'Jane Doe') {
elements.removeAttr('class');
}
},
};
},
};
});
由於我們幾乎不會用到 compile
和 pre
這兩個 hook,所以在 AngularJS 中,提供了 link
讓我們可以直接操作到 post
hook:
myApp.directive('searchResult', function () {
return {
// ...
// 等同於把 compile 刪掉,並把裡面的 post 拿出來後改名成 link
link: function (scope, elements, attrs) {
console.log('Post-linking...');
if (scope.personObject.name === 'Jane Doe') {
elements.removeAttr('class');
}
},
};
});