此篇文使用的各工具版本為:
- Angular CLI: 18.0.1
- Node: 20.13.1
- Package Manager: npm 10.5.2
Angular-CLI:
好用的 Angular 官方工具,可用 node.js 使用
npm install -g @angular/cli
安裝。
以下介紹下一些指令:
檢查 Angular-CLI, Angular 等版本:
ng version
ng new 指令可用來建立初始 Angular 專案檔案結構:
ng new {專案名稱}
ng serve 指令可以開啟預設的 http://localhost:4200 簡易 server 來測試網頁
ng serve
ng generate 指令可建立各種 Angular 元件,例如 Component, Directive
等,例:ng generate component {component 名稱}
ng generate component {component 名稱}
ng generate directive {directive 名稱}
ng generate service {service 名稱}
編譯手包佈署時要用的最終程式,詳細參數可參考 ng build • Angular:
ng build
-------------------------------------------------------------------------------------------------------------
Standalone Component:
Angular 14 後推出 Standalone Component (也包括
Standalone Directive 等)
並主推它們,
之後使用 Angular-CLI
建立元件時會預設使用 Standalone。
使用 Standalone Component 的好處是其不須依附於 NgModule 上。
以往我們會使用 NgModule 來管理各個 Component, Directive 等,
但在例如
Component 的 HTML 中如果使用了某另一個 Component 時,
我們會比較難找到是用了哪一個
Component,例如:
AppComponent 的 Component Class:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
}
AppComponent 的 HTML:
<xxx-component></xxx-component>
其中光看 AppComponent 我們無法馬上看出 <xxx-component> 到底是哪個
component,
這代表我們要去找到 AppCompoment 所屬的的 NgModule ,
去看看
app.module.ts 中 declarations 了哪些 Component,
然後一個個去看那些
Component 有哪個的 selector 是 xxx-comonent,
例:
main.ts:
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
app.module.ts:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { OtherComponent1 } from './other1.component';
import { OtherComponent2 } from './other2.component';
// 可能更多 .........
@NgModule({
declarations: [
AppComponent,
OtherComponent1,
OtherComponent2
// 可能更多 .........
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
但如果使用 Standalone Component,我們就不須要 NgModule、不需要
app.module.ts,
我們可以直接把 Standalone Component 常成根元件而不是
NgModule。
而使用 Standalone Component 的 Component 必須顯示的 import Standalone
Component 進來,
例如假設 <xxx-component> 的 Component 是一個
Standalone Component,
,其 Class 長得像這樣,注意到 @Component
修飾子裡有加上 standalone: true:
import { Component } from '@angular/core';
@Component({
selector: 'xxx-component',
standalone: true,
imports: [],
templateUrl: './app.xxx-component.html',
styleUrl: './app.xxx-component.scss'
})
export class AppXxxComponent {
}
上例 AppComponent 的 Component Class 就要改寫成:
import { Component } from '@angular/core';
import { XxxComponent } from './xxx-component.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [XxxComponent],
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
}
可以看到必須 imports XxxComponent 才能使用 XxxComponent 這個 Standalone
Component,
這時我們就可以很清楚地知道 AppComonent 使用了
XxxComponent,
不用再辛苦地去找 XxxComponent 到底在哪裡。
另一個可以注意到的是 AppComponent 也加了 standalone: true,
所以
AppComponent 也是一個 Standalone Component,
我們就可以把 AppComponent
直接當作 Root Component 設定給 main.ts,像這樣:
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
-------------------------------------------------------------------------------------------------------------
Interpolations
(插值、或稱內嵌):
使用雙重大括弧 {{}} 來設定 Interpolations,以顯示
component 裡的 data,
例:
HTML:
{{myData}}
-------------------------------------------------------------------------------------------------------------
Property Binding, Attribute Binding, Class and Style Binding:
這三種
Binding 都是使用中括弧來設定,且都是單向綁定,
當 HTML DOM
上的值被改變時,不會影響到 Component 中對應物件的值。
例如 <input
type="text" [value]="inputValue"/> 的 Value 被使用者在畫面中修改後,
Component
中的 inputValue 物件值並不會被改變。
下面各別介紹:
Property Binding:
綁定 DOM 的 Property,注意跟 Attribute 不同,例如
<td> 的跨行 Property 是 colSpan,而對應的 Attribute 是 colspan。
範例:
import { Component } from '@angular/core';
@Component({
selector: 'app-xxx',
standalone: true,
imports: [],
template: `
<a [href]="link">Link</a>
<img [src]="imgSrc"/>
<input type="text" [value]="inputValue"/>
<table>
<tr>
<td [colSpan]="1+1">xxx</td>
</tr>
<tr>
<td>yyy</td>
<td>yyy</td>
</tr>
</table>
`,
styleUrl: './xxx.component.scss'
})
export class XxxComponent {
link = "https://www.cyberlink.com";
imgSrc = "https://dl-file.cyberlink.com/web/stat/edms/prog/bar/img/Cyberlink.svg";
inputValue = "xxx";
}
Attribute Binding:
綁定 Attribute,用 [attr.{要設定的 Attribute Name}]
來表示,可以跟上面 Property Binding 做比較,這邊要用 Attribute 的 colspan
而不是 Property 的 colSpan。
範例:
import { Component } from '@angular/core';
@Component({
selector: 'app-xxx',
standalone: true,
imports: [],
template: `
<div [attr.aria-label]="ariaLabel">xxx</div>
<table>
<tr>
<td [attr.colspan]="1+1">xxx</td>
</tr>
<tr>
<td>yyy</td>
<td>yyy</td>
</tr>
</table>
`,
styleUrl: './xxx.component.scss'
})
export class XxxComponent {
ariaLabel = "xxx";
}
Class and Style Binding:
綁定 Class Name 和
Style,可以接受多種輸入,詳細可以參考官方文件
Class and style binding • Angular,
例如單個 Class Name 可以接受 Boolean 值做輸入、
多個 Class
Name 可以用以空格分隔的 Class List String 或 Map (Key 代表 Class Name,Value
為 Boolean 值,代表要不要設定這個 Class)。
單個 Style 可以接受 String,
多個 Style 可以接受 inline css 的
String
或是 Map (Key 要用給 Javascript 用的 Style Property
Name,不可以用給 CSS StyeSheet 用的 Property Name)
範例:
import { Component } from '@angular/core';
@Component({
selector: 'app-xxx',
standalone: true,
imports: [],
//templateUrl: './xxx.component.html',
template: `
<div [class.xxx-class-name]="isAddXxxClass">XXX</div>
<div [class]="classListStr">XXX</div>
<div [class]="classList">XXX</div>
<div [class]="classMap">XXX</div>
<div [style.background-color]="backgroundColor">xxx</div>
<div [style.backgroundColor]="backgroundColor">xxx</div>
<div [style]="styleListStr">xxx</div>
<div [style]="styleMap">xxx</div>
`,
styleUrl: './xxx.component.scss'
})
export class XxxComponent {
isAddXxxClass = true;
classListStr = "class-1 class-2";
classList = ["class-1", "class-2"];
classMap = {
"class-1" : true,
"class-2" : false
}
backgroundColor = "#ff00ff";
styleListStr = "background-color: #ff00ff; font-size: 20px;";
styleMap = {
backgroundColor : "#ff00ff",
fontSize : "20px"
};
}
-------------------------------------------------------------------------------------------------------------
Event Binding (事件綁定):
使用小括弧 () 來設定 Event Binding,類似於
AngularJS 的 ng-{Event 名} (ng-click, ng-change, etc....),
例:
import { Component } from '@angular/core';
@Component({
selector: 'app-xxx',
standalone: true,
imports: [],
template: `<input type="text" (input)="onInput($event)"/>
<button (click)="onClick()">Button</button>`,
styleUrl: './xxx.component.scss'
})
export class XxxComponent {
onInput(event: Event) {
console.log("Inpute Event.");
console.log((event.currentTarget as HTMLInputElement).value);
}
onClick() {
console.log("Click Event.");
}
}
-------------------------------------------------------------------------------------------------------------
Two Way Data Binding (雙向綁定):
可參考官方文件 Two-way binding • Angular 和 Directives • Overview - ngModel • Angular。
先來看比較原生的實作方式會比較好理解其中的原理。
比如我們現在有兩個 Component,Parent 和 Child,
ParentComponent
裡面使用了 ChildComponent,
ParentComponent 把一個 Property 的值輸入至
ChildComponent 中,
ChildComponent 會在值被改變時主動用 EventEmitter
送出 Event 出來給 ParentComponent,
ParentComponent
收到後就可以做相應的處理,
而 [(xxx)]="yyy" 語法是 [xxx]="yyy"
(xxxChange)="yyy = $event;"
的合併簡易寫法。
範例:
Parent Component :
import { Component } from '@angular/core';
import { ChildComponent } from '../child/child.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `
<app-child [xxx]="inputValue" (xxxChange)="inputValue = $event;"></app-child>
<app-child [(xxx)]="inputValue"></app-child>
`,
styleUrl: './parent.component.scss'
})
export class ParentComponent {
inputValue = "";
}
Child Component :
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
imports: [],
template: `
<input type="text" [value]="xxx" (input)="onInput($event)"/>
`,
styleUrl: './child.component.scss'
})
export class ChildComponent {
@Input() xxx!: string;
@Output() xxxChange = new EventEmitter<string>();
onInput(event: Event) {
var value = (<HTMLInputElement>event.currentTarget).value;
this.xxxChange.emit(value);
}
}
實作上通常會使用 ngModel 來幫忙,跟上述一樣可以用非簡易跟簡易的兩種作法:
-
非簡易作法:比較麻煩的作法,但有時想在賦值前做一些處理時可用到,
例如先把使用者在<input>中輸入的值改成全大寫後再賦值給
Component 的 Property 值。
因為有用到 ngModel (其中寫好了 event
向外傳遞的過程等),要 Import FormModule 才能使用,
先把 Component 的
Property 輸入給 ngModel 這個 Angular 內建的 Directive,
ngModel
偵測值的改變,當值發生改變時用 EventEmitter 把值用 EventEmitter
向外送出一個 ngModelChange 的 Event 給外層 Component,
然後外層
Component 接受到 ngModelChange 事件後,可以由 $event 得到改變後的值,
這時我們就可以執行我們要的操作。
範例:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-xxx',
standalone: true,
imports: [FormsModule],
//templateUrl: './xxx.component.html',
template: `
<input type="text" [ngModel]="inputValue" (ngModelChange)="inputValue = $event;"/> {{inputValue}}
`,
styleUrl: './xxx.component.scss'
})
export class XxxComponent {
inputValue = "xxx";
}
-
簡易作法:比較簡潔的作法,並且也是官方的推薦雙向綁定寫法,
使用
Angular 提供的 Banana in Box 語法,用 [(ngModel)]="xxx" 來實現,
是上一個麻煩寫法的簡易寫法,一樣有用到
ngModel,所以要 import FormModule。
範例:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-xxx',
standalone: true,
imports: [FormsModule],
//templateUrl: './xxx.component.html',
template: `
<input type="text" [(ngModel)]="inputValue"/>
`,
styleUrl: './xxx.component.scss'
})
export class XxxComponent {
inputValue = "xxx";
}
-------------------------------------------------------------------------------------------------------------
Template
Reference Variable (樣板參考變數):
使用 # 來標識 Template Reference
Variable,
就可以在 Component Class
中建立一個指向特定對像的變數,根據標識的地方可以指向不同的對像,例如:
- 如果在 Component (元件)上聲明變數,該變數就會引用該組件實例。
- 如果在標準的 HTML DOM 標記上聲明變數,該變數就會引用該元素。
-
如果你在 <ng-template> 元素上聲明變數,該變數就會引用一個
TemplateRef 實例來代表此樣板。
-
如果該變數在右側指定了一個名字,比如
#var="ngForm",那麼該變數就會指向標識的元素上具有這個 exportAs 名字的
Directive 或 Component。
可以參考
[Angular 大師之路] exportAs - 取得 directive 實體的方法 |
全端開發人員天梯
例如一個 Directive 可以設定 exportAs 來讓 Host DOM 所處的 Component
可以存取 Directive Instnace。
範例:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Directive, HostBinding } from '@angular/core';
@Directive({
selector: '[xxx-directive]',
standalone: true,
exportAs: "xxxDirective"
})
export class MyDirectiveDirective {
//會為此 Directive 依附的 DOM 加上 background-color style
@HostBinding("style.backgroundColor") backgroundColor = "#00FF00";
constructor() { }
sayHello() {
console.log("Hello!");
}
}
@Component({
selector: 'app-xxx',
standalone: true,
imports: [FormsModule, MyDirectiveDirective],
template: `
<div xxx-directive #abc="xxxDirective" (click)="abc.sayHello()">XYZ</div>
`,
styleUrl: './xxx.component.scss'
})
export class XxxComponent {
}
詳細可參考 Angular - 理解樣板變數
例下面的範例可以在 <button> 被按下時改變 #myTemplateVariable 標識的
<p> 的 innerHTML:
<p #myTemplateVariable>xxxxx ......</p>
<button (click)="myTemplateVariable.innerHTML = 'xxxxx clicked'">click me</button>
下面這個範例 Component 有 Import Form Module,
而其中的 ngForm 這個
Directive 因為有設定 <form> 為 selector 的關係,
所以
<form> 上會存在 ngForm 這個 Directive ,
而因為 ngForm
Directive 本身有設定 exportAs: "ngForm" 的關係,
所以使用
#myForm="ngForm" 就可以指向在 <form> 上面的 ngForm
Directive,進而取得 valid 等參數或去操作 form。
#firstName 和 #lastName 分別指向其上的 ngModel
** 注意:如果沒寫 "ngForm" ,只寫了 #myForm 的話,myForm 就會變成只指向
<form> 這個 HTML element DOM:
<form #myForm="ngForm">
<div>First Name : <input type="text" name="firstName" [(ngModel)]="formData.firstName" #firstName="ngModel" required/></div>
<div style="color: #ff0000;" [hidden]="firstName.valid || firstName.pristine">First name is required!</div>
<div>Last Name : <input type="text" name="lastName" [(ngModel)]="formData.lastName" #lastName="ngModel" required/></div>
<div style="color: #ff0000;" [hidden]="lastName.valid || lastName.pristine">Last name is required!</div>
<div>Is From valid: {{myForm.form.valid}}</div>
</form>
-------------------------------------------------------------------------------------------------------------
Directives:
可用 Angluar CLI 的
ng generate directive {Directive 名稱}
Directive 跟 Component 不一樣,沒有它自己的 template ,
主要是依附在其他的 Componenet 或者是 DOM 上,
selector 就是 css
selector,可以指定 class, attribute 等,
通常是用來對被依附的 DOM
進行各項操作,
例如加新的 class 之類的,
這裡示範下 Attribute Directive 的用法。
(Angular 內建的 *ngIf,
*ngFor 也是一種 Directive ,不過是屬於 Structural Directives)。
範例:
import { Component, Directive, ElementRef, HostBinding, HostListener, Input } from '@angular/core';
@Directive({
selector: '[xxx-directive]',
standalone: true,
exportAs: "xxxDirective"
})
export class XxxDirective {
//使用 @HostBinding 設定一開始 color 的範例
//可以跟 @Input 一起用,此例讓 defaultColor 可以接受來自 Host 來的值
@HostBinding("style.color") @Input() defaultColor = "#FF0000";
//可以用 ElementRef.nativeElement 來取得宿主 (Host) DOM 的 HtmlElement 物件
constructor(private elementRef: ElementRef) {//
}
//可以使用 @HostListener 來設定宿主 DOM 的 EventBinding
@HostListener("mouseenter") onMouseenter() {
this.elementRef.nativeElement.style.backgroundColor = "#00FF00";
}
@HostListener("mouseleave") onMouseleave() {
this.elementRef.nativeElement.style.backgroundColor = "";
}
sayHello() {
console.log("Hello!");
}
}
@Component({
selector: 'app-xxx',
standalone: true,
imports: [XxxDirective],//
//templateUrl: './xxx.component.html',
template: `
<div xxx-directive defaultColor="#0000FF" #xxxDir="xxxDirective" (click)="xxxDir.sayHello()">XYZ</div>
`,
styleUrl: './xxx.component.scss'
})
export class XxxComponent {
}
這裡示範 Structural Directive,例如 *ngIf 和 *ngFor ,
要注意必須先 import CommonModule
(或是各別 import NgIf 和 NgFor) 才能正常使用。
Structural Directive 跟 Attribute Directive 不一樣的地方是它主要會改變 DOM 結構,
實作使用 ng-template 配合 Directive 來達成,而 *ngIf, *ngFor 這種寫法是 Angular 的語法糖,
自己也可以自己實作客製的 Structural Directive,詳細可參考 Structural directives • Angular 和 [Angular 大師之路] 自己的樣板語法自己做 (Structural Directives) | 全端開發人員天梯:
Component Class:
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-my-component',
standalone: true,
imports: [CommonModule],
templateUrl: './my-component.component.html',
styleUrl: './my-component.component.scss'
})
export class MyComponentComponent {
isShow: boolean = true;
myItemList: {id: number, name: string}[] = [{
id: 1,
name: "11"
},{
id: 2,
name: "22"
},{
id: 3,
name: "33"
}];
}
my-component.component.html:
<div *ngIf="isShow">*ngIf test</div>
<li *ngFor="let myItem of myItemList">
{{myItem.id}} : {{myItem.name}}
</li>
-------------------------------------------------------------------------------------------------------------
@-Syntax
for Control Flow:
Angular 17 推出了 @-Syntax for Control Flow,
可以以更底層、不使用
Directive 的方式做出一樣的功能,
而且不用依附在 DOM
上面,以下示範介紹 @if, @else if, @else, @for, @switch 等:
Component Class:
import { Component } from '@angular/core';
@Component({
selector: 'app-xxx',
standalone: true,
imports: [],
templateUrl: './xxx.component.html',
styleUrl: './xxx.component.scss'
})
export class XxxComponent {
condition1: boolean = true;
condition2: boolean = false;
str = "A";
strA = "A";
strB = "B";
myItemList: {id: number, name: string}[] = [{
id: 1,
name: "11"
},{
id: 2,
name: "22"
},{
id: 3,
name: "33"
}];
}
my-component.component.html:
@if (condition1) {
<p>condition 1</p>
}
@else if (condition2) {
<p>condition 2</p>
} @else {
<p>other conditions</p>
}
@switch (str) {
@case (strA) {
<p>A</p>
}
@case (strB) {
<p>B</p>
}
@default {
<p>other</p>
}
}
@for (myItem of myItemList; track myItem.id) {
<li>{{myItem.id}} : {{myItem.name}}</li>
}
-------------------------------------------------------------------------------------------------------------
@Input:
當一個 Parent Component 使用了另一個 Child Component,
並且
Parent Component 想傳遞某個值進 Child Component,
並且想要在 Child
Component 中顯示、操作修改這個值時,
我們就要在 Child Component 中使用
@Input 來告訴 Angular 這個值是從外部傳入的。
例如:
my-component.component.ts (我們的 Parent Component Class):
import { Component } from '@angular/core';
import { SubComponentComponent } from '../sub-component/sub-component.component';
@Component({
selector: 'app-my-component',
standalone: true,
imports: [SubComponentComponent],
templateUrl: './my-component.component.html',
styleUrl: './my-component.component.scss'
})
export class MyComponentComponent {
myItemList: {id: number, name: string}[] = [{
id: 1,
name: "11"
},{
id: 2,
name: "22"
},{
id: 3,
name: "33"
}];
selectedMyItem = this.myItemList[1];
}
my-component.component.html:
<app-sub-component [item]="selectedMyItem"></app-sub-component>
sub-component.component.ts (我們的 Child Component Class):
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-sub-component',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './sub-component.component.html',
styleUrl: './sub-component.component.scss'
})
export class SubComponentComponent {
@Input() item?: {id: number, name: string};
}
sub-component.component.html:
<div *ngIf="item">
Id: <input type="text" [(ngModel)]="item.id"/><br/>
Name: <input type="text" [(ngModel)]="item.name"/>
</div>
這邊要注意一下,Child Component 的 item 在 Angular 一開始建構各元件時,
可能會是 undefined 的,所以我們要用 *ngIf (或@if) 來設定有 item 值時才顯示
item,
不然 typescript 編譯時會有
NG2: Object is possibly 'undefined'.
錯誤。
-------------------------------------------------------------------------------------------------------------
@Output:
@Input 的用途是讓 Parent Component 向 Child Component 輸入資料,
而 @Output 剛好相反,是用來讓 Child Component 向 Parent Component 送出資料,
在前面 Two-way Binding 的範例有用到,這邊再貼一次,
主要就是用 @Output 修飾 Child Component 的 EventEmitter property,
然後 Child Component 主動地用 EventEmitter 將資料以 Event 的形式向 Parent Component 送,
Parent Component 就可以用 Event Binding 的方式接收資料。
範例:
Parent Component :
import { Component } from '@angular/core';
import { ChildComponent } from '../child/child.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `
<app-child [xxx]="inputValue" (xxxChange)="inputValue = $event;"></app-child>
<app-child [(xxx)]="inputValue"></app-child>
`,
styleUrl: './parent.component.scss'
})
export class ParentComponent {
inputValue = "";
}
Child Component :
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
imports: [],
template: `
<input type="text" [value]="xxx" (input)="onInput($event)"/>
`,
styleUrl: './child.component.scss'
})
export class ChildComponent {
@Input() xxx!: string;
@Output() xxxChange = new EventEmitter<string>();
onInput(event: Event) {
var value = (<HTMLInputElement>event.currentTarget).value;
this.xxxChange.emit(value);
}
}
-------------------------------------------------------------------------------------------------------------
Service (服務):
可以用 Angular-CLI 的以下指令建立:
ng generate service {Service 名稱}
Service 會使用 @Injectable 的 Annotation (註解) 來設定,
我們可以把
Service 注入到多個不同的 Component Constructre function 中,
這樣就可以讓多個
Component 共用同個 Service 來共享資訊、
或者把資料的取得移到 Service
裡來讓 Component 可以專注在自己的其他邏輯功能上。
範例:
my-service.service.ts (我們建立的 Service) :
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MyServiceService {
constructor() { }
getDataList(): {id: number, name: string}[] {
return [{
id: 1,
name: "11"
},{
id: 2,
name: "22"
},{
id: 3,
name: "33"
}];
}
}
Component Class (使用 Service 取得資料):
import { Component, OnInit } from '@angular/core';
import { MyServiceService } from '../service/my-service.service';
@Component({
selector: 'app-my-component',
standalone: true,
imports: [],
templateUrl: './my-component.component.html',
styleUrl: './my-component.component.scss'
})
export class MyComponentComponent implements OnInit {
myItemList: {id: number, name: string}[] = [];
selectedMyItem?:{id: number, name: string};
constructor(private myService: MyServiceService) {
}
ngOnInit(): void {
this.myItemList = this.myService.getDataList();
this.selectedMyItem = this.myItemList[1];
}
}
-------------------------------------------------------------------------------------------------------------
Observable (可觀察):
Angular 有很多異步操作 (非同步操作 或稱 Asynchronous) 都利用到了
RxJs 的
Observable ,
其類似於 Javascript 的 Promise、 JQuery 的 Deferred, Promise 或 AngularJS 的
$q, promise,
可以處理 Asynchronous 非同步的動作,例如非同步的 HttpGet
資料取得,
將資料包裹在 Observable 物件中,
然後我們就可以用 Observable 的 subscribe() 來非同步的處理動作。
範例:
my-component.component.ts :
import { Component, OnInit } from '@angular/core';
import { MyServiceService } from '../service/my-service.service';
@Component({
selector: 'app-my-component',
standalone: true,
imports: [],
templateUrl: './my-component.component.html',
styleUrl: './my-component.component.scss'
})
export class MyComponentComponent implements OnInit {
myItemList: {id: number, name: string}[] = [];
selectedMyItem?:{id: number, name: string};
constructor(private myService: MyServiceService) {
}
ngOnInit(): void {
this.myService.getDataListAsync().subscribe((dataList: {id: number, name: string}[]) => {
this.myItemList = dataList;
this.selectedMyItem = this.myItemList[1];
});
}
}
my-service.service.ts:
import { Injectable } from '@angular/core';
import { Observable, delay, of} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class MyServiceService {
constructor() { }
getDataListAsync(): Observable<{id: number, name: string}[]> {
let dataList: {id: number, name: string}[] = [{
id: 1,
name: "11"
},{
id: 2,
name: "22"
},{
id: 3,
name: "33"
}];
return of(dataList).pipe(delay(5000)); //模擬網路傳輸,延遲 5 秒再送出資料
}
}
-------------------------------------------------------------------------------------------------------------
Routing (路由):
Routing (路由) 可以指定不同的 url 去載入不同的 Component ,作用類似於 AngularJS
的 <ui-view>, $urlRouterProvider, $stateProvider
那些,下面示範:
main.ts (程式主要進入點):
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
在 main.ts 中,可以看到 bootstrapApplication() 設定了一個作為 root component
的 AppComponent 和一個 config 設定用的 appConfig,我們來看一下 appConfig
的內容:
app.config.ts :
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
};
app.config.ts 把一個實作了 ApplicationConfig interface export 出來,用到了
provideRouter(routes),其中 routes 就是我們要來設定 Route Rule (路由規則)
的地方,先來看一下 app.routes.ts 的內容。
app.routes.ts (路由規則) :
import { Routes } from '@angular/router';
import { MyComponentComponent } from './my-component/my-component.component';
import { MyComponent2Component } from './my-component-2/my-component-2.component';
export const routes: Routes = [
{
path: "my-component",
component: MyComponentComponent
}, {
path: "my-component-2",
component: MyComponent2Component
}, {
path: "",
redirectTo: "my-component",
pathMatch: "full"
}
];
這裡我們設定了預設會轉到的 path 是 /my-component,然後當 path 是 /my-component
時使用 MyComponentComponent 這個 Component,當 path 是 /my-component-2 時使用
MyComponent2Component。
我們先看一下 MyComponentComponent 和 MyComponent2Component 的內容。
my-component.component.ts (就是 MyComponent):
import { Component } from '@angular/core';
@Component({
selector: 'app-my-component',
standalone: true,
imports: [],
template: `<p>my-component works!</p>`,
styleUrl: './my-component.component.scss'
})
export class MyComponentComponent {
}
my-component-2.component.ts (就是 MyComponent2Component ) :
import { Component } from '@angular/core';
@Component({
selector: 'app-my-component-2',
standalone: true,
imports: [],
template: `<p>my-component-2 works!</p>`,
styleUrl: './my-component-2.component.scss'
})
export class MyComponent2Component {
}
兩者的內容都一樣,只是在 html template 上印出不同句子以示區別而已。
接著最後就是作為 root 的 AppComponent
app.component.ts (就是 AppComponent,為了) :
import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
import {MyComponentComponent} from './my-component/my-component.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, RouterLink, MyComponentComponent],
//templateUrl: './app.component.html', //也可以把 html 寫在另一個 html 檔裡
template: `
<div><a routerLink="/my-component">My Component</a></div>
<div><a routerLink="/my-component-2">My Component 2</a></div>
<router-outlet></router-outlet>
`,
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'my-angular-application';
}
在 AppComponent 中,因為此例使用了 routerLink 和 <router-outlet>
,所以我們要 import RouterLink 和 RouterOutlet 進來。
routerLink 可以作為 DOM 的屬性來設定對應到 Route 規則的 path 到 <a>
上,<a> 會被賦予相應的 href,當 <a>
被按下時,就會改變瀏覽器網址列的網址,
而當網址符合設定的 Route
規則時,
<router-outlet> 就會載入相應的 Component。
-------------------------------------------------------------------------------------------------------------
使用 ActivatedRoute 讀取 url path 中的參數:
使用 ActivatedRoute
可以讀我們方便的讀取 url path 中的參數,
例如如果我們有 Route
規則設定了如下:
app.routes.ts:
import { Routes } from '@angular/router';
import { MyComponentComponent } from './my-component/my-component.component';
import { MyComponent2Component } from './my-component-2/my-component-2.component';
export const routes: Routes = [
{
path: "my-component/:id",
component: MyComponentComponent
}, {
path: "my-component-2",
component: MyComponent2Component
}, {
path: "",
redirectTo: "my-component/1",
pathMatch: "full"
}
];
path 中的冒號(:)表示 :id 是一個佔位符,它符合像 /my-component/1,
/my-component/2 等這樣的 path。
接著在 MyComponent 中我們可以如下地使用
ActivateRoute.snapshot.paramMap.get("id") 取出 :id 佔位符的值,如
/my-component/1 這個 path 的 id 就會是 1、/my-component/2 就是 2。
my-component.component.ts :
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-my-component',
standalone: true,
imports: [],
template: `<p>my-component works!</p>`,
styleUrl: './my-component.component.scss'
})
export class MyComponentComponent {
constructor(private route: ActivatedRoute) {
}
ngOnInit(): void {
console.log(this.route.snapshot.paramMap.get("id")); // 印出 url path 中 :id 佔位符的實際值
}
}
-------------------------------------------------------------------------------------------------------------
使用 Location 可以操作一些跟瀏覽器相關的操作,例如回到上一頁
(跟直接連到上一頁的網址不同,會直接影響瀏覽歷史紀錄,就跟瀏覽器的上一頁操作一樣),一樣以
MyComponent 為例,下例的 goBack() 函式如果被呼叫的話就可以回到上一頁:
my-component.component.ts :
import { Component } from '@angular/core';
import { Location } from '@angular/common';
@Component({
selector: 'app-my-component',
standalone: true,
imports: [],
template: `<p>my-component works!</p>`,
styleUrl: './my-component.component.scss'
})
export class MyComponentComponent {
constructor(private location: Location) {
}
goBack(): void {
this.location.back();
}
}
-------------------------------------------------------------------------------------------------------------
HttpClient:
Angular 提供了 HttpClient 工具來幫助進行 HTTP 呼叫,以 MyComponent 為例:
my-component.component.ts:
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
@Component({
selector: 'app-my-component',
standalone: true,
imports: [],
template: `<p>my-component works!</p>`,
styleUrl: './my-component.component.scss'
})
export class MyComponentComponent implements OnInit{
constructor(private http: HttpClient) {
}
ngOnInit(): void {
this.getData().then((data) => {
console.log(data);
});
this.sendData();
}
getData(): Promise<string> {
//HttpClient.get() 回傳的是 RxJs 的 Observable,要呼叫 Observable 的 subscribe() 才會執行 HTTP GET 呼叫,
//所以這裡為了之後可以利用 subscribe(data) 的 data ,使用了 Promise 把 http.get() 包起來並把 data 放入 resolve(data) 中,
//之後可以用 Promise.then((data) => {}) 來得到 data。
return new Promise<string>((resolve) => {
this.http.get("https://xxx.xxx.xxx/xxx", { responseType: "text" }) //沒給 responseType 的話預設是 JSON
.subscribe((data: string) => {
resolve(data);
});
});
}
sendData(): Promise<void> {
//HttpClient.post() 回傳的是 RxJs 的 Observable,要呼叫 Observable 的 subscribe() 才會執行 HTTP POST 呼叫
//所以這裡為了將 subscribe(data) 的 data 回傳,使用了 Promise 把 http.get() 包起來並把 data 回傳。
//這邊展示如果 resolve() 沒有想要塞值的話,在 typescript 裡可以用 new Promise<void> 來指定 void ,不然 typescript 會報錯。
return new Promise<void>((resolve) => {
this.http.post("https://yyy.yyy.yyy/yyy", {data: "tttest"})
.subscribe(() => {
resolve();
});
});
}
}
在 MyComponent 中我們先在 constructor 中注入了 HttpClient 來使用,示範了利用
HttpClient 來進行 HTTP GET 和 HTTP POST 的呼叫,
需要注意到的是,HttpClient.get()
和 HttpClient.post() 回傳的是 RxJs 的 Observable ,屬於 Cold Observable ,不像
Hot Observable 不管有沒有被 subscribe() 都會執行內容, Cold Observable
需要被執行 subscribe() 才會執行內容,詳細可以參考這篇 [RxJS] Cold Observable v.s Hot Observable | 全端開發人員天梯,比較是屬於 RxJS 的東西。
-------------------------------------------------------------------------------------------------------------
Form (表單) 相關
Angular 的 Form 可以用兩種方法來製作,分成 Template-Driven Form (樣板驅動表單)
和 Reactive Form (回應式表單) 兩種,
Template-Driven Form 跟 AngularJs
的方法較為相似,適合簡單的 Form,實作也較簡單易懂。Reactive Form 適合較複雜的
Form,實作一些複雜功能會較有彈性,複用性比較高、也較易於測試。下面分別示範:
Template-Driven Form (樣板驅動表單) :
可參考 Angular - 建立樣板驅動表單
下面直接給範例程式,說明都放在註解中
my-form.component.ts
import { Component } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
@Component({
selector: 'app-my-form',
standalone: true,
imports: [FormsModule], /* 使用到了 FormsModule */
templateUrl: './my-form.component.html',
styleUrl: './my-form.component.scss'
})
export class MyFormComponent {
formData: { firstName: string, lastName: string } = {
firstName: "",
lastName: ""
};
submitForm(form: NgForm) {
console.log("submit!");
console.log(form.form.valid);
//因為 NgForm 有把 NgForm.form 上很多屬性複製到 NgForm 本身上,
//所以直接用 NgForm.valid 也可以存取 NgForm.form.valid
console.log(form.valid);
console.log(form.value.firstName);
console.log(form.value.lastName);
//NgForm 有些好用的 function,例如重置 Form 狀態的 reset()
form.reset();
}
}
my-form.component.html
<p>my-form works!</p>
<!-- 使用 Template Reference Variable, #myForm 來得到 ngForm Directive 的實體對像 -->
<!-- 在 form 被 submit 時觸發 ngSubmit Event,在這裡可以將 ngForm Directive 實體傳入 -->
<form #myForm="ngForm"
(ngSubmit)="submitForm(myForm)">
<!-- 跟 #myForm 類似,這裡是用 #firstName 取得 ngModel 的實體對像 -->
<!-- 用 [(ngModel)] 設定雙向繫結 -->
<div>First Name : <input type="text" name="firstName"
[(ngModel)]="formData.firstName"
#firstName="ngModel"
required/></div>
<!-- 利用得到的名為 firstName 的 ngModel 來存取其 valid, pristine 等值 -->
<div style="color: #ff0000;"
[hidden]="firstName.valid || firstName.pristine">
First name is required!
</div>
<div>Last Name : <input type="text" name="lastName" [(ngModel)]="formData.lastName" #lastName="ngModel" required/></div>
<div style="color: #ff0000;" [hidden]="lastName.valid || lastName.pristine">Last name is required!</div>
<div>Is Form valid: {{myForm.valid}}</div>
<div>
<!-- 利用取得的名為 myForm 的 ngForm 存取 form 的 valid 等值,也可以用 myForm.valid,因為 form 上的大部份屬性在 ngForm 上都有一部份複本 -->
<button type="submit"
[disabled]="!myForm.form.valid">
Submit
</button>
</div>
</form>
Reactive Form (回應式表單):
可參考
Angular - 回應式表單
my-reactive-form.component.ts
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormArray, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { first } from 'rxjs';
@Component({
selector: 'app-my-reactive-form',
standalone: true,
imports: [ReactiveFormsModule, CommonModule], //在這裡要使用 ReactiveFormsModule
templateUrl: './my-reactive-form.component.html',
styleUrl: './my-reactive-form.component.scss'
})
export class MyReactiveFormComponent implements OnInit {
myForm!: FormGroup;
constructor(private formBuilder: FormBuilder) {
}
ngOnInit(): void {
//設定 FormGroup 和 FormControl,跟 Template-Driven Form 不同,
//Form 的各元件顯示地被建立在 Javascript code 中,
//可以很方便的操作和控制。
//值得注意的是,FormGroup 也是可以塞進 FormGroup, FormArray 中的。
this.myForm = new FormGroup({
firstName: new FormControl("", Validators.required),
lastName: new FormControl("", [Validators.required, Validators.maxLength(10)]),
address: new FormGroup({
country: new FormControl("", Validators.required),
city: new FormControl("", Validators.required)
}),
favorites: new FormArray([
new FormControl("", Validators.required)
])
});
//也可以用 FormBuilder 幫助用較簡化的方式設定 FormGroup 和 FormControl,
//作用跟上面的程式完全一樣
/*
this.myForm = this.formBuilder.group({
firstName : ["", Validators.required],
lastName : ["", [Validators.required, Validators.maxLength(10)]],
address: this.formBuilder.group({
country : ["", Validators.required],
city : ["", Validators.required]
}),
favorites: this.formBuilder.array([
this.formBuilder.control("", Validators.required)
])
});
*/
}
//在這邊設定一個 getter 來讓我們在 html 中可以直接用 favoritesFormArray 這個屬性名直接存取 favorites 這個 FormArray
//像是這樣: <div *ngFor="let favorite of favoritesFromArray.controls; let i = index;"></div>
//**因為不能寫成這樣: <div *ngFor="let favorite of (myForm.get('favorites') as FromArray)?.controls; let i = index;"></div>
get favoritesFromArray(): FormArray {
return this.myForm.get("favorites") as FormArray;
}
//實作動態加入 FormControl 到 FormArray 中
addMoreFavoriteField() {
//(this.myForm.get("favorites") as FormArray).push(new FormControl("", Validators.required));
//因為有設定 favoritesFromArray 這個 getter,所以也可以用下面寫法達到一樣效果
this.favoritesFromArray.push(new FormControl("", Validators.required));
}
//實作動態從 FormArray 中移除 FormControl
removeFavoriteField(index: number) {
//(this.myForm.get("favorites") as FormArray).removeAt(index);
//因為有設定 favoritesFromArray 這個 getter,所以也可以用下面寫法達到一樣效果
this.favoritesFromArray.removeAt(index);
}
//測試 FormGroup.setValue()
setValueTest() {
//FormGroup.setValue() 可以設定子元件的值,如果結構不對 (例如 address 沒用 Object 格式) 或有其他問題會有 Error
this.myForm.setValue({
firstName : "FirstName",
lastName : "LastName",
address : "country" //這裡 address 的型別錯了,應該是要物件才對,所以執行會有 Error
});
}
//測試 FormGroup.patchValue()
patchValueTest() {
//Formgroup.patchValue() 也可以設定子元件的值,但它會盡可能的去設定,如果結構不對 或有其他問題也不會有 Error
this.myForm.patchValue({
firstName : "FirstName",
lastName : "LastName",
address : "country" //這裡 address 的型別錯了,應該是要物件才對,但執行不會有 Error,會直接惣略此欄位
});
}
submitForm() {
console.log(this.myForm.value);
}
}
my-reactive-form.component.html
<p>my-reactive-form works!</p>
<!-- 設定 formGroup, myForm 就是我們在 Component 中設定的 FormGroup 物件 -->
<!-- 在 form 被 submit 時觸發 -->
<form [formGroup]="myForm"
(ngSubmit)="submitForm()">
<!-- 設定 formControlName, firstName 就是我們在 Component 中設定的名為 myForm 的 FormGroup 中的名為 firstName 的 FormControl 物件 -->
<div>First Name : <input type="text" name="firstName" formControlName="firstName"/></div>
<!-- 用 FormGorup.get(FromControl 的名字) 取得其下的各 FormControl,再取得各 Form control 的 valid, pristine 等值 -->
<!-- 也可以用 FormGroup.controls 來取得各 FormControl -->
<div style="color: #ff0000;"
[hidden]="myForm.get('firstName')?.valid || myForm.controls['firstName'].pristine">
First name is required!
</div>
<div>Last Name : <input type="text" name="lastName" formControlName="lastName"/></div>
<div style="color: #ff0000;" [hidden]="myForm.get('lastName')?.valid || myForm.get('lastName')?.pristine">Last name is required!</div>
<!-- 用 formGroupName 設定 FormGroup 中的 FormGroup 成員 -->
<div formGroupName="address">
<div>Country : <input type="text" name="country" formControlName="country"/></div>
<div>City : <input type="text" name="city" formControlName="city"/></div>
</div>
<div>Address Form Status: {{myForm.get('address')?.status}}</div>
<div>Is From valid: {{myForm.valid}}</div>
<div>Form Status: {{myForm.status}}</div>
<!-- 設定 FormArray,其中的 FormController 可以不用有 Key 名稱 -->
<div formArrayName="favorites">
<div *ngFor="let favorite of favoritesFromArray.controls; let i = index;">
<!-- FormArray 中的各子項元件是用 Array index 去做 key,所以要用 index 做 fromControlName 的設定, -->
<!-- formControlName 外的中括弧代表等號右邊是 Expression ,而非純 String -->
Favorite {{i}} : <input type="text" [formControlName]="i"/>
<button (click)="addMoreFavoriteField()">Add</button>
<button (click)="removeFavoriteField(i)">Remove</button>
</div>
</div>
<div><button (click)="setValueTest()">Set value test</button></div>
<div><button (click)="patchValueTest()">Patch value test</button></div>
<div>
<!-- 利用取得的名為 myForm 的 ngForm 存取 form 的 valid 等值,也可以用 myForm.valid,因為 form 上的大部份屬性在 ngForm 上都有一部份複本 -->
<button type="submit"
[disabled]="!myForm.valid">
Submit
</button>
</div>
</form>