2024年7月8日 星期一

Angular 語法、功能、工具、指令等紀錄

此篇文使用的各工具版本為:

  • 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 來幫忙,跟上述一樣可以用非簡易跟簡易的兩種作法:

  1. 非簡易作法:比較麻煩的作法,但有時想在賦值前做一些處理時可用到,
    例如先把使用者在<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";
    }
  2. 簡易作法:比較簡潔的作法,並且也是官方的推薦雙向綁定寫法,
    使用 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 中建立一個指向特定對像的變數,根據標識的地方可以指向不同的對像,例如:

  1. 如果在 Component (元件)上聲明變數,該變數就會引用該組件實例。
  2. 如果在標準的 HTML DOM 標記上聲明變數,該變數就會引用該元素。
  3. 如果你在 <ng-template> 元素上聲明變數,該變數就會引用一個 TemplateRef 實例來代表此樣板。
  4. 如果該變數在右側指定了一個名字,比如 #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 名稱}

分成
Attribute Directive (可參考 Attribute directives • Angular)
Structural Directive (可參考 Structural directives • Angular)

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>

沒有留言 :

張貼留言