跳至主要内容

[course] Learn and Understand AngularJS

此篇為各筆記之整理,非原創內容,主要資料來源為 Learn and Understand AngularJS @ Udemy

Model, View, Whatever

在 HTML 中使用 ng-appng-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

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>

從 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');
}
},
};
},
};
});

由於我們幾乎不會用到 compilepre 這兩個 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');
}
},
};
});

Giscus