此篇文使用的各工具版本為:
- 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 {專案名稱}
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,
例:
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));
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>
可用 Angluar CLI 的
ng generate directive {Directive 名稱}
Attribute Directive (可參考 Attribute directives • Angular)
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 一開始建構各元件時,
不然 typescript 編譯時會有
錯誤。
而 @Output 剛好相反,是用來讓 Child Component 向 Parent Component 送出資料,
範例:
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 注入到多個不同的 Component Constructre function 中,
這樣就可以讓多個 Component 共用同個 Service 來共享資訊、
或者把資料的取得移到 Service 裡來讓 Component 可以專注在自己的其他邏輯功能上。
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]; } }
RxJs 的 Observable ,
可以處理 Asynchronous 非同步的動作,例如非同步的 HttpGet 資料取得,
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 秒再送出資料 } }
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));
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)] };
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" } ];
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 { }
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 { }
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>
沒有留言 :
張貼留言