# Angular 介紹

# 安裝 Angular

安裝 @angular/cli

npm i -g @angular/cli

查看 angular 版本

ng version
# Angular CLI: 13.3.2

安裝 vs code angular 套件 套件:Angular Extension Pack

# 建立 AngularCli

建立一個 AngularCli 專案

ng new demo

會詢問你幾個問題

# 是不是要加入 routing
? Would you like to add Angular routing? Yes
# css 預處理器選擇
? Which stylesheet format would you like to use? SCSS [https://sass-lang.com/documentation/syntax#scss]

再用 vscode 打開專案資料夾 (demo) 輸入開啟專案

npm start

# Angular 目錄結構用途

src > app > 主要寫程式的地方 assets > 靜態檔案(css, js, icon, images)

# Angular 基本語法

app.module.ts

// angular module
@NgModule({
  // view component
  declarations: [AppComponent],
  // import Module
  imports: [BrowserModule, AppRoutingModule],
  // import service
  providers: [],
  // 啟動根元件
  bootstrap: [AppComponent],
})
// dircrator
export class AppModule {}

app.component.ts

// 元件 dircrator
@Component({
  // tag 名字 <app-root></app-root>
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})

# 加入一個 component 到 app

# 加入 component 到 app 下面
ng generate component page1
# 簡寫
ng g c page1

會一個 component 資料夾裡面會有四個檔案

  • page1.component.css
  • page1.component.html
  • page1.component.spce.ts
  • page1.component.ts

另外會註冊 page1 檔案到 app.modules.ts

若要使用在 app 使用 page1 元件 在 app.component.html 加入以下 tag 即可使用 page1 元件

...
<app-page1></app-page1>
...

查詢建立不同類型的 component

ng generate -h
# 可以使用以下的 component
# app-shell
# application
# class
# component
# directive
# enum
# guard
# interceptor
# interface
# library
# module
# pipe
# resolver
# service
# service-worker
# web-worker

# 加入靜態資料到 Angular

加入 api folder, assets folder, blog-index.html 靜態檔案複製到 src 資料夾內,並且重新啟動網站

source code (opens new window)

如果直接在網址上面打 http://localhost:4200/blog-index.html 並不會看到頁面,需在 angular.json 檔案設定 assets 資料才可以使用,在 projects > demo > architect > build > options > assets 內新增

angular.json

"assets": [
  "src/blog-index.html",
  "src/api",
  "src/favicon.ico",
  "src/assets"
]

新增完後再重啟網站(npm start),在網址列輸入 http://localhost:4200/blog-index.html, http://localhost:4200/api/db.json 看網頁是否可以正常顯示,若可以顯示代表設定沒問題,如果無法顯示需確認是不是哪裡有打錯。

接下來將靜態網頁 blog-index.html 合併到 Angular 專案下,head 的內容不會包含在 component 裡面,所以 head 裏面的設定需放入 index.html 檔案內

將 index.html 內的 head 留下 base tag 其餘的資料刪除,且貼上位置需在 base 之後。base 為整份網頁所有的超連結預設的基礎。如果貼在 base 之前有可能找不到 js, css 的檔案導致網站壞掉

<base href="/" />
<!-- head 程式碼貼在這邊 -->
<meta
  name="viewport"
  content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="/assets/styles/mainapp.css" rel="stylesheet" />

接下來複製 blog-index.html body 到 app.component.html 裡面貼上 (tip: 使用 mac 電腦在 vscode 可以使用快速鍵 commamd + p 打開搜尋資料面板,輸入 appcom 搜尋檔案) 貼完後回到首頁看一下有沒有問題

ok 有報錯打開 fn + F12 看一下 console 錯誤碼,將 hgroup tag 改成 div 即可,因為 Angular 只能接受 html5 的 tag。

# 部署 Angular

在終端機輸入

ng build

在資料夾裡面會新增一個 dist 資料夾,打開 index.html 此時檔案是以壓縮過的檔案,按下 option + shift + F 讓檔案重新排版

在 dist 資料夾發現 blog-index.html 但這個並非開發用,只是為了貼上版型,這樣需回到 angular.json 把 blog-index.html 刪除

"assets": [
  "src/api",
  "src/favicon.ico",
  "src/assets"
]

# Angular 版本升級

使用終端機輸入以下指令,查看 angular 版本,如果版本叫就終端機會顯示警告黃字

ng version

使用終端機輸入以下指令,則可以自動更新 angular 版本,主要是更新 angular.json 再重新下載新版本的檔案

ng update

更新 angular cli 使用 npm 查看 global 第一層安裝的檔案

npm list -g --depth=0
# @angular/cli@13.3.2

# 查看目前所安裝在本機 global 套件版本與最新版本
npm outdated -g

# 更新 global 套件
npm install -g @vue/cli @angular/cli

# 範本參考變數 (Template reference variables)

DOM

  • 範本參考變數可以用在任意的 HTML tag 上面,語法為 #name = ref-name
  • 會在範本內建立一個名為 name 的區域變數
  • name 的區域變數會儲存該標籤的 DOM 物件
  • 可以透過 「事件繫結」 將 DOM 物件中屬性回傳元件的類別中

    因為 DOM 物件會回傳到元件類別中,所以取名時需注意不可以一樣,以免抓錯值

<!-- #tKeyword 以免元件屬性相同,可以加入前綴字 -->
<input
  type="text"
  #tKeyword
  [(ngModel)]="keyword"
  (keyup.escape)="keywordReset()"
  placeholder="請輸入搜尋關鍵字"
  accesskey="s"
/>
<!-- 可以把 keyword.length 改成 tKeyword.value.length -->
<span>字串長度:{{ keyword.length }} 個字</span>
<span>字串長度:{{ tKeyword.value.length }} 個字</span>

directive (component)

  • 如果寫在 directive 上的話,就可以使用該元件的屬性與方法

首先先建立一個 header 的 component

  ng g c header

然後把 app.component.html 裡面的 header 搬到 header.component.html 裡面

header.component.html

<header class="header">...</header>

app.component.html

...
<!-- #tKeyword 以免元件屬性相同,可以加入前綴字 -->
<app-header #tHeader></app-header>
<section class="container" (click)="tHeader.title = 'Title Change'">
  ...
</section>

這樣寫以後在 section click 的時候就會改變 header.component.html 裡的 title 屬性

範本參考數變數只能在範本使用,在預設情況下無法在元件讀取

# 安全導覽運算子 safe navigation operator(?.)

從 Sever (後端)拿資料時有可能該欄位沒有值 (Linebot) 導致版面顯示有問題,若使用安全導覽運算子(?.) 代表該資料可有可無,不會讓頁面版面壞掉

<a [href]="item.href">{{ item?.title | lowercase | slice:0:20 }}</a>

Angular 安全導覽運算子只能使用在 html 的 template 裡

# Template 型別錯誤

<!-- 可能會報錯 Angular service language  -->
<a [href]="item.href">{{ item.subject?.title }}</a>
<!-- 改寫成下面這樣 -->
<a [href]="item.href">{{ item['subject']?.title }}</a>

# Angular 元件架構

Angular 是由各種 Component 組成

父子元件可以使用使用屬性繫結、事件繫結來傳遞資料

Service 透過相依注入(DI) 注入到特定 Component 裡面

當專案越大時就會把相關的元件(Component, Service)分割成 Module(功能模組,Feature Module)

# 模組化架構

建立一個 Module ,在終端機輸入,Angular CLI 會自動幫我們建立 article.module.ts

ng g m article

建立完 Module 後須引入到 app.module.ts 裡面

app.module.ts

import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";

import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { FormsModule } from "@angular/forms";
// import ArticleModule
import { ArticleModule } from "./article/article.module";

@NgModule({
  declarations: [AppComponent],
  // import ArticleModule
  imports: [BrowserModule, AppRoutingModule, FormsModule, ArticleModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

另外一種建立方式

建立 module 並自動引入到 app module 裡(感覺方便好用=D)

ng g m article -m app

將 Article 拆解成元件 ArticleList > ArticleHeader, ArticleBody 並引入到 ArticleModule 裡面

首先

要在 Article 資料夾建立 ArticleList 元件,直接在 vscode folder 按下右鍵選擇“在整合式終端機開啟”

此時開啟終端機的位置就會在 Article 資料夾下,打以下指令即可建立 article-list 元件

ng g c article-list

建立完之後需在 ArticleModule exports ArticleListComponent (這樣在 app template 才能使用)

@NgModule({
  declarations: [
    ArticleListComponent
  ],
  imports: [
    CommonModule
  ],
  exports:[
    ArticleListComponent
  ]
})

接下來把 app.component.html 的 article 搬入 article-list 裡,並在 app.component.html 加入 app-article-list tag

app.component.ts data 搬入 article-list.component.ts 裡

在 article-list 下建立 article-header, article-body (一樣需注意終端機的位置,在 article 資料夾下)

ng g c article-header
ng g c article-body

且要注意這裡的 article-header, article-body 不需在 ArticleModule exports 出來,因為它們隸屬於 article-list 內(只需 exports article-list)

一樣把 header, body 放入該放的 template 裡,並將 tag 寫入 article-list 即可(此時因為元件化導致資料找不到報錯,後續使用@Input 來處理)

在 article-header, article-body 的 ts 內加入 @Input 讓資料傳入子層,記得要 import Input 進來才能用

import { Component, OnInit, Input } from "@angular/core";

@Component({
  selector: "app-article-body",
  templateUrl: "./article-body.component.html",
  styleUrls: ["./article-body.component.scss"],
})
export class ArticleBodyComponent implements OnInit {
  @Input() item;
  constructor() {}
  ngOnInit(): void {}
}

然後在 article-list 裡使用屬性繫結綁定父層資料到子層

<article
  class="post"
  id="post{{idx}}"
  *ngFor="let item of data;let idx = index"
>
  <!-- [父層變數]="子層變數" -->
  <app-article-header [item]="item"></app-article-header>
  <app-article-body [item]="item"></app-article-body>
</article>

# 生命週期

export class ArticleHeaderComponent implements OnInit {
  @Input() item;
  // 建構式 > class 被建立時執行 > 基本上不會寫程式在裡面
  // 元件上未初始化,故無法使用屬性,但會使用相依注入 DI
  constructor() {}
  // 元件 property binding 已完成,可以對一些屬性給初始值或發出 ajax 跟後端拿取資料
  ngOnInit(): void {}
  // 銷毀前的 hook, only 1% 使用到,rxjs subscribe() 可能會使用到
  ngOnDestroy() {}
}

接下來改一下 article-list.component.ts 的資料,先定義一個變數 data 在 ngOnInit() 給這個 data 一個初始值

export class ArticleListComponent implements OnInit {
  // 定義變數
  data;
  constructor() { }
  ngOnInit(): void {
    // 給初始值
    this.data = [
     ...
    ]
  }
}

# @Output

在 article-header 裡面新增一個刪除按鈕

<header>
  ...
  <span>
    <button (click)="deleteList">刪除文章</button>
  </span>
  ...
</header>

因為資料由父層傳送到子層,無法直接由子層刪除資料,處理的方式為按下子層的按鈕後通知父層刪除資料,所以子層按鈕只有通知的功能,並無刪除的功能,刪除需由父層來操作

在子層定義一個 @Output 送出一個 EventEmitter 事件發射器出來,EventEmitter import 時要注意從 '@angular/core' import 進來

在 button click 時觸發 deleteList() 的方法

import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";

export class ArticleHeaderComponent implements OnInit {
  @Input()
  item: any;
  // 子層定義一個 EventEmitter
  @Output()
  delete = new EventEmitter<any>();

  constructor() {}

  ngOnInit(): void {}
  deleteList() {
    // 使用 EventEmitter 裡的 emit 傳 this.item 出去
    this.delete.emit(this.item);
  }
}

在 article-list 裡新增事件 (delete) 當子層 click button 時會觸發 (delete) 讓父層執行 doDelete($event),$event 為子層傳出來的值

article-list.component.html

<article
  class="post"
  id="post{{idx}}"
  *ngFor="let item of data;let idx = index"
>
  <app-article-header
    [item]="item"
    (delete)="doDelete($event)"
  ></app-article-header>
  <app-article-body [item]="item"></app-article-body>
</article>

article-list.component.ts

doDelete(item:any){
  console.log(item);
}

此時只要點擊按鈕以後就會觸發父層 doDelete($event),$event 回傳的是 *ngFor 跑的 item

改寫一下 doDelete()

在寫改寫 data.filter 時後面沒有提示訊息,將 data 定義成 Array 當 data. 就可以使用陣列的方法

export class ArticleListComponent implements OnInit {
  data: Array<any> = [];

  doDelete(item: any) {
    this.data = this.data.filter((data: any) => {
      return item !== data;
    });
  }
}

接下來嘗試看看可不可以刪除資料,可以刪除代表就成功了