Angular Fetch CRUD

Html

綁定表單 [(ngModel)]與 使用

[ngClass]=”{‘disabled’:item.isEdit}”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<ng-container class="container">
<div class="title">
<h1 >{{Title}}</h1>
<img [src]="imageLink" [alt]="imageAlt" [title]="imageAlt">
</div>
<div class="forms">
<div class="input-group">
<input type="text" class="form-control"
[(ngModel)]="userForm.name" placeholder="姓名"
/>
<input type="text" class="form-control" placeholder="電子郵件"
[(ngModel)]="userForm.email"
/>
<input type="password" class="form-control" placeholder="密碼"
[(ngModel)]="userForm.password"
/>
<input type="text" class="form-control" placeholder="城市"
[(ngModel)]="userForm.address"
/>
<input type="text" class="form-control" placeholder="鄉鎮"
[(ngModel)]="userForm.country"
/>
</div>
<button class="btn btn-primary" (click)="addItem()">Add</button>
</div>
<div class="container">
<table class="table table-striped">
<thead>
<tr>
<th scope="col">項目</th>
<th scope="col">姓名</th>
<th scope="col">電子郵件</th>
<th scope="col">城市</th>
<th scope="col">區域</th>

</tr>
</thead>
<tbody>

<!-- @for (post of posts; track post.id) {-->
<tr *ngFor="let item of posts ; let index = index" >
<td>
<p > {{item.user_id}}</p>
</td>
<td>
<input *ngIf="item.isEdit" [(ngModel)]="item.name"/>
<p *ngIf="!item.isEdit"> {{item.name}}</p>
</td>
<td>
<input *ngIf="item.isEdit" [(ngModel)]="item.email"/>
<p *ngIf="!item.isEdit"> {{item.email}}</p>
</td>
<td>
<input *ngIf="item.isEdit" [(ngModel)]="item.country"/>
<p *ngIf="!item.isEdit"> {{item.country}}</p>
</td>

<td>
<input *ngIf="item.isEdit" [(ngModel)]="item.address"/>
<p *ngIf="!item.isEdit"> {{item.address}}</p>
</td>

<td>
<button class="btn btn-warning" (click)="editPost(item.user_id,index)" >Edit</button>
<button class="btn btn-danger ml-2" (click)="remotePost(item.user_id,index)" [ngClass]="{'disabled':item.isEdit}">Delete</button></td>
</tr>
<!-- }-->
</tbody>
</table>
</div>
</ng-container>

.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import { Component, inject, OnInit } from '@angular/core';
// Angular 17以後必須引入才能在Html 使用*ngFor
import {CommonModule} from '@angular/common';
import { FormsModule } from '@angular/forms'
interface listType{
id: string | never,
subject: string,
editTask: string,
isShow:boolean
}
interface userFormType{
user_id?: number |null | never |any,
name?: string|any|null ,
email?: string|any|null ,
password?: string|any|null ,
address?: string|any|null ,
country?: string|any|null ,
}
@Component({
selector: 'app-fetch-data',
standalone: true,
imports: [
FormsModule,
CommonModule,// Angular 17以後必須引入
],
templateUrl: './fetch-data.component.html',
styleUrl: './fetch-data.component.scss'
})
export class FetchDataComponent implements OnInit {
Title: string = 'To Do Lists';
imageAlt:string= '圖片';
imageLink: string = "https://cdn-icons-png.flaticon.com/512/4697/4697260.png";
userForm: userFormType = {
user_id: null,
name: "",
email: "",
password: "",
address: "",
country: "",
}

lists: listType[] = [];
newTask: string = "";//表單綁定
isAvalible: boolean = false;
posts: any = [];
isEdit: boolean = false;
url: string = 'https://api-node-mysql-project.vercel.app/users';
header:any= {
'Content-Type': 'application/json',
}
// 新增
addItem() {
const api="https://api-node-mysql-project.vercel.app/users"
const vm = this;
let query = {
name: vm.userForm.name,
email: vm.userForm.email,
password: vm.userForm.password,
address: vm.userForm.address,
country: vm.userForm.country
// id:Date.now().toString(),
}
fetch(api, { method: 'POST',headers:vm.header , body: JSON.stringify(query)})
.then((res) => res.json())
.then((data) => {
vm.isAvalible = true;
console.log('新增的參數', query, data)
vm.posts.push(query);
this.userForm.name = ""
this.userForm.address = ""
this.userForm.password = ""
this.userForm.country=""
});
vm.isAvalible = true;
}

//獲取
fetchGetValue() {
const vm = this;
const api="https://api-node-mysql-project.vercel.app/users"
fetch(api,{method: 'GET' })
.then((res) => res.json())
.then((data) => {
data.map((item: any, index: any) => {
let query = {
user_id: item.user_id,
name: item.name,
email: item.email,
password: item.password,
address: item.address,
country: item.country,
isEdit: false,
}
return vm.posts.push(query)
});

console.log( 'vm.posts', vm.posts )
});
}

// 編輯
editPost(user_id: any,index:any) {
const vm = this;
vm.posts[index].isEdit = true;
const api = `https://api-node-mysql-project.vercel.app/users/${user_id}`
let query = {
name: vm.posts[index].name,
email: vm.posts[index].email,
password: vm.posts[index].password,
address: vm.posts[index].address,
country: vm.posts[index].country,
// id:Date.now().toString(),
}
fetch(api, {
method: 'PUT',
headers: vm.header,
body: JSON.stringify(query)
})
.then((res) => res.json())
.then((data) => {
console.log('changedRows', data.result.changedRows,'屬性',typeof data.result.changedRows);
if (data.result.changedRows===1) {
alert('編輯成功')
vm.posts[index].isEdit = false;
}
});
}
// 刪除
remotePost(user_id: any,index:any) {
console.log(user_id);
const api = `https://api-node-mysql-project.vercel.app/users/${user_id}`
fetch(api, {method: 'DELETE',headers: this.header})
.then((res) => res.json())
.then((data) => {
console.log(data);
if (data.result.insertId === 0) {
this.posts.splice(index, 1);
alert(user_id+'刪除成功')
}
});
}
ngOnInit(): void {
this.fetchGetValue()
}
}

fetch Get

1
2
3
4
5
6
7
8
9
10
fetch(api)
.then((res) => {
return res.json();
})
.then( (res) => {
console.log(res);
})
.catch((error) => {
console.log(`Error: ${error}`);
})

fetch Post

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 header:any= {
'Content-Type': 'application/json',
}
fetch(api, {
method: 'POST',
headers:{'Content-Type': 'application/json'} ,
body: JSON.stringify(query)})
.then((res) => res.json())
.then((data) => {
let query={
title:'這是新增'
}
this.posts.push(query);
});

Angular CRUD

Angular 雙向綁定 [(ngModel)]

綁定表單 [(ngModel)]

[(ngModel)]必須引入FormsModule

[ngClass]=”{‘disabled’:item.isEdit}”

[ngClass]必須引入NgClass
Html
1
2
3
4
<input *ngIf="item.isEdit" [(ngModel)]="item.name"/>
<p *ngIf="!item.isEdit"> {{item.name}}</p>

<button class="btn btn-danger ml-2" (click)="remotePost(item.user_id,index)" [ngClass]="{'disabled':item.isEdit}">Delete</button>

Ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component, OnInit } from '@angular/core';
+ import { NgIf } from '@angular/common';
+ import { FormsModule } from '@angular/forms';

@Component({
selector: 'app-login',
standalone:true,
+ imports: [ FormsModule ,NgIf],
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
})

export class LoginComponent implements OnInit {

ngOnInit(): void {}
}

Angular CRUD

Angular Error

錯誤訊息:[ERROR] NG8001: ‘app-to-do’ is not a known element

在app資料夾內app.component.html 新增

1
2
3
4
<main class="main">
<app-to-do></app-to-do>
</main>
<router-outlet />
錯誤訊息:[ERROR] NG8001: 'app-to-do' is not a known element
處理方式:imports引入 ToDoComponent
在app資料夾內引入app.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
+ import { ToDoComponent } from './components/to-do/to-do.component';

@Component({
selector: 'app-root',
standalone: true,
imports: [
RouterOutlet,
+ ToDoComponent
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'tode_project';
}

Error: getaddrinfo ENOTFOUND localhost

處理方式

Mac 系統前往

1
/private/etc/
將hosts檔案移動到桌面修改
1
27.0.0.1 localhost

錯誤訊息 Can’t bind to ‘ngModel’ since it isn’t a known property of ‘input’

無法綁定到“ngModel”,因為它不是“input”的已知屬性

處理方式
引入 FormsModule
1
2
3
4
5
6
7
8
9
10
11
import { FormsModule } from '@angular/forms';

[...]

@NgModule({
imports: [
[...]
FormsModule
],
[...]
})

錯誤訊息 The pipe ‘async’ could not be found

處理方式
引入 CommonModule
1
import { CommonModule } from '@angular/common';

錯誤訊息 打包時 [WARNING] Module ‘dayjs’ used by ‘src/app/home/home.component.ts’ is not ESM

處理方式
angular.json 檔案內 allowedCommonJsDependencies(允許 CommonJs 依賴項):["dayjs"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 {
"projects":{
"vite_project":{
"architect":{
"build":{
"options":{
+ "allowedCommonJsDependencies": [
+ "dayjs"
+ ],
}
}
}
}
}
}

錯誤訊息 Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0.

Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0.
More info and automated migrator: https://sass-lang.com/d/import
The plugin “angular-sass” was triggered by this import

處理方式

src/style.scss引入方式@import修改為@use

1
@use 'bootstrap/scss/bootstrap';

錯誤訊息 打包時 Did not expect successive traversals.

▲ [WARNING] 9 rules skipped due to selector errors:
.table>>> -> Did not expect successive traversals.
.table-sm>>> -> Did not expect successive traversals.
.table-bordered>>* -> Did not expect successive traversals.
.table-bordered>>> -> Did not expect successive traversals.
.table-borderless>>> -> Did not expect successive traversals.
.form-floating>label -> Did not expect successive traversals.
.form-floating>
label -> Did not expect successive traversals.
.btn-group>+.btn -> Did not expect successive traversals.
.btn-group>+.btn -> Did not expect successive traversals.

處理方式
angular.json 檔案內
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"projects":{
"vite_project":{
"architect":{
"build":{
+ "options":{
+ "optimization": {
+ "scripts": true,
+ "styles": {
+ "minify": true,
+ "inlineCritical": false
+ },
+ "fonts": true
+ }
}
}
}
}
}
}

錯誤訊息 打包時 bundle exceeded maximun. Budget 500kb was not met by ….

捆綁包超出最大值。預算 500 kb 未達
如圖

處理方式
angular.json 檔案內 budgets捆綁initial最初 與 anyComponentStyle任何組件樣式 =>maximumWarning 最大警告 =>maximumError最大誤差
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"projects":{
"vite_project":{
"architect":{
"build":{
"configurations": {
"production": {
"budgets":[
{
"type": "initial",
+ "maximumWarning": "50MB",
+ "maximumError": "10MB"
},
{
"type": "anyComponentStyle",
+ "maximumWarning": "50MB",
+ "maximumError": "10MB"
}
]
}
}
}
}
}
}
}

錯誤訊息:ERROR NullInjectorError: R3InjectorError(Environment Injector)[_ProductsService -> _ApiService -> _HttpClient -> _HttpClient]:

處理方式如下
src/app/app.config.ts 引入 provideHttpClient
1
2
3
4
5
6
7
8
9
10
11
12
13
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
+ import {provideHttpClient} from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
+ provideHttpClient(),
provideRouter(routes)
]
};

✘ [ERROR] The ‘index/product/:id’ route uses

✘ [ERROR] The ‘index/product/:id’ route uses prerendering and includes parameters, but ‘getPrerenderParams’ is missing. Please define ‘getPrerenderParams’ function for this route in your server routing configuration or specify a different ‘renderMode’.

處理方式
1
2
3
4
5
6
7
8
9
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
{
path: '**',
+ renderMode: RenderMode.Server
}
];

Angular Router路由的使用

設定路由

一前置作業新增頁面

新增Home與fetchData 頁面
新增Home頁面
1
2
3
// 新增元件指令
ng g c 頁面或是功能
ng g c Home
產生了home 資料夾內有以下檔案
1
2
3
4
home.component.html
home.component.scss
home.component.spec.ts
home.component.ts
新增user 頁面
1
ng g c User
產生了ng g c User 資料夾內有以下檔案
1
2
3
4
user.component.html
user.component.scss
user.component.spec.ts
user.component.ts
新增PageNotFound 頁面
1
ng g c PageNotFound
產生了page-not-found 資料夾內有以下檔案
1
2
3
4
page-not-found.component.html
page-not-found.component.scss
page-not-found.component.spec.ts
page-not-found.component.ts
二 在 app.routes.ts檔案設定
  • 引入Home,Users,User,PageNotFound元件,解構寫法必須加名稱Component 大駝峰式寫法
  • export const routes: Routes = [path: '路徑', component: 頁面元件]
  • 首頁 { path: "", redirectTo:'home',pathMatch:'full' },
  • 404頁 { path: '**', component: PageNotFoundComponent }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Routes } from '@angular/router';
//引入,解構寫法必須加名稱Component 大駝峰式寫法
import { HomeComponent } from './home/home.component';
import { UsersComponent } from './users/users.component';
import { UserComponent } from './user/user.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

export const routes: Routes = [
{ path: "", redirectTo:'home',pathMatch:'full' },
{ path: "home", component:HomeComponent },
{ path: "users", component: UsersComponent},
{ path: "user/:userId", component: UserComponent},
{ path: '**', component: PageNotFoundComponent }, // 404頁面
];

配置應用程式 在app.config.ts檔案中新增以下設定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';

+ import { routes } from './app.routes';
import { provideClientHydration } from '@angular/platform-browser';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';

export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
+ provideRouter(routes),
provideClientHydration(),
provideAnimationsAsync()
]
};

this.router.navigate([‘users’]); 切換至頁面

  • 引入Router
  • constructor( private router: Router, ) { ... }
  • this.router.navigate([path網址])
1
2
3
4
5
6
7
8
9
10
11
12
import {  Router } from '@angular/router';

constructor(
private router: Router,
) {
...
}

viewProduct( item: any): void {
console.log(item)
this.router.navigate(["/index/product/"+item._id]);
}
### 元件的引入(Navba主選單的切換) 到components新增
1
2
// ng g c 頁面或是功能
ng g c Navbar
產生了navbar 資料夾內有以下檔案
1
2
3
4
navbar.component.html
navbar.component.scss
navbar.component.spec.ts
navbar.component.ts
利用 routerLink 指令實作頁面切換 navbar.component.html檔案內新增
  • routerLink是routes內的path
  • routerActive="active"
1
2
3
4
5
6
7
8
9
10
11
<nav class="nav">
<ul>
<li>
<a routerLink="/" routerActive="active" class="cursor-pointer">Home</a>
</li>
<li>
<a routerLink="fetch-data" routerActive="active" class="cursor-pointer">FetchData</a>
</li>
</ul>
</nav>

navbar.component.ts
引入RouterLink
import { RouterLink} from '@angular/router';
@Component({
imports: [
RouterLink
],
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Component } from '@angular/core';
+ import { RouterLink } from '@angular/router';
@Component({
selector: 'app-navbar',
standalone: true,
imports: [
+ RouterLink
],
templateUrl: './navbar.component.html',
styleUrl: './navbar.component.scss'
})
export class NavbarComponent {

}

在首頁載入

在 app.component.ts
引入NavbarComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
+ import { NavbarComponent } from './components/navbar/navbar.component';
import { environment } from '../environments/environment';

@Component({
selector: 'app-root',
imports: [
RouterOutlet,
+ NavbarComponent,
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = environment.production ? 'Production' : '開發中';
}

嵌套路由

1
<router-outlet></router-outlet>

在 app.component.html 會使用 , 元件來定義頁面所需要顯示的位置

1
2
3
4
5
<app-navbar></app-navbar>
<main class="main">
<router-outlet />
</main>

router 案例github

404 頁面

如果要顯示自訂 404 頁面,請設定通配符路由,並將元件屬性設定為PageNotFoundComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Routes } from '@angular/router';
//引入,解構寫法必須加名稱Component 大駝峰式寫法
import { HomeComponent } from './home/home.component';
import { UsersComponent } from './users/users.component';
import { UserComponent } from './user/user.component';
+ import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

export const routes: Routes = [
{ path: "", component: HomeComponent },
{ path: "users", component: UsersComponent},
{ path: "user/:userId", component: UserComponent},
+ { path: '**', component: PageNotFoundComponent }, // 404頁面
];

router 初次設定

動態 Router

以UserComponent(user資料夾user.component.ts) 為例
  • 載入@angular/core 的inject 載入@angular/router 的 ActivatedRoute,
  • private activeRouter = inject(ActivatedRoute);
  • this.activeRouter.snapshot.paramMap.get('userId')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+  import { Component, OnInit, inject } from '@angular/core';
+ import { CommonModule } from '@angular/common';
+ import { ActivatedRoute } from '@angular/router';

@Component({
selector: 'app-user',
imports: [
+ CommonModule,// Angular 17以後必須引入
],
templateUrl: './user.component.html',
styleUrl: './user.component.scss'
})
+ export class UserComponent implements OnInit{
+ private activeRouter = inject(ActivatedRoute);
+ ngOnInit(): void {
+ this.getDetailID();
+ }

+ getDetailID(): void {
+ const userId = this.activeRouter.snapshot.paramMap.get('userId');
+ console.log('userId', userId);
+ }
}

模組化且延遲載入,提升更好的使用者體驗

Angular 19 App with Lazy Loading
隨著專案的擴大,路由設定會越來越大,元件也會越來越多

  • loadComponent:() => import('檔案位置').then((m)=> m.HomeComponent)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

import { Routes } from '@angular/router';

import { UserComponent } from './user/user.component';

export const routes: Routes = [
{ path: "", redirectTo: 'home', pathMatch: 'full' },
{
path: "home",
loadComponent:() => import('./home/home.component').then((m)=> m.HomeComponent)
},
{
path: "users",
loadComponent:() =>import('./users/users.component').then((m)=>m.UsersComponent)
},
{
path: "user/:userId",
component: UserComponent
},
{
path: "login",
loadComponent:() =>import('./login/login.component').then((m)=>m.LoginComponent)
},
{
path: "test_login",
loadComponent:()=>import('./test-login/test-login.component').then((m)=>m.TestLoginComponent)
},
{
path: "admin",
loadComponent:() =>import('./admin/index-admin/index-admin.component').then((m)=>m.IndexAdminComponent) ,
children: [
{
path: "admin_home",
loadComponent:()=>import('./admin/look/look.component').then((m)=>m.LookComponent)
},
{ path: '', redirectTo: 'admin_home', pathMatch: 'full' },
]
},
{
// 404頁面
path: '**',
loadComponent: () => import('./page-not-found/page-not-found.component').then((m) => m.PageNotFoundComponent)
},
];

Angular 列表渲染與判斷

渲染列表

*ngFor渲染列表 let item of todoDataList;let index = index,index 為索引
1
2
3
4
5
6
7
8
<ul *ngFor="let item of todoDataList;let index = index">
<li >
{{index +1}}
</li>
<li>
{{item.title}}
</li>
</ul>

判斷是否顯示與渲染列表

to-do.component.html

@if(isAvalible){}@else{}:判斷是否顯示
@for(i of tasks; track $index){}渲染列表
按鈕上(click)="addTasks()" 函式的綁定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<div class="lists">
@if(isAvalible){
<table class="table table-striped">
<thead>
<tr>
<th scope="col">項目</th>
<th scope="col">事項</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody>
@for (item of lists; track $index) {
<tr>
<td>
{{item.id}}
</td>
<td>
@if(item.isShow){
<input type="text" [(ngModel)]="item.editTask"/>
}@else{
{{item.subject}}
}
</td>
<td>
<button class="btn btn-warning" (click)="editTasks($index,item.editTask)">Edit</button>
<button class="btn btn-danger ml-2" (click)=" deleteTasks($index)">Delete</button></td>
</tr>
}

</tbody>
</table>
}
</div>

@if{}@else{}

在to-do.component.ts引入FormsModule
  • 引入FormsModuleimport { FormsModule } from '@angular/forms'
    因為ngModel是隸屬於FormsModule模組下的套件,所以要import FormsModule進來
  • importsFormsModule
  • export class ToDoComponent 內的資料定義與函式
    lists: listType[] = [];
    newTask: string = "";//表單綁定
    isAvalible : boolean = false;
    addTasks()函式綁定在Button
    addTasks()函式的邏輯判斷
事件繫結
input事件:輸入框偵測到任何輸入都會觸發 同時要帶入$event 事件參數,$event 是詳細描述此次事件之各種數值的物件 有幾種Evnet方式可以玩看看
  • (input): 偵測任何輸入時觸發
  • (keyup): 離開鍵盤按鍵時觸發
  • (blur): 焦點(當前鼠標或鍵盤聚焦之處)離開輸入框時觸發
  • (change): 焦點離開輸入框、並且值被改變時才觸發
參考資料
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { Component } from '@angular/core';
+ import { FormsModule } from '@angular/forms'
@Component({
selector: 'app-to-do',
standalone: true,
imports: [
+ FormsModule
],
templateUrl: './to-do.component.html',
styleUrl: './to-do.component.scss'
})

export class ToDoComponent {
Title: string = 'To Do Lists';
imageLink: string = "https://cdn-icons-png.flaticon.com/512/4697/4697260.png";
+ tasks: string[] = [];
+ newTask: string = "";//表單綁定
isAvalible : boolean = false;

//新增
+ addTasks() {
const vm = this;
//新增的參數
let query = {
id:Date.now().toString(),
subject: vm.newTask.trim(),
editTask: vm.newTask.trim(),
isShow:false,
}
console.log('新增的參數',query)
vm.lists.push(query);
vm.isAvalible = true;
}
//刪除
deleteTasks(index: number) {
const vm = this;
vm.lists.splice(index,1);
vm.isAvalible = vm.lists.length > 0;
}
//編輯
editTasks(index: number, editTask: string): string | void {
const vm = this;
vm.lists[index].isShow = true;
console.log(editTask)
vm.lists[index].subject = editTask;
console.log(vm.lists);
vm.newTask ="";
}
}

安裝primeng

1
npm install --save primeng 

全局引入 MatButtonModule與ButtonModule

在app.component.ts
  • 引入MatButtonModule
  • 引入ButtonModule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
+ import {MatButtonModule} from '@angular/material/button';
+ import { ButtonModule } from 'primeng/button';
import { ToDoComponent } from './components/to-do/to-do.component';

@Component({
selector: 'app-root',
standalone: true,
imports: [
RouterOutlet,
+ MatButtonModule,
+ ButtonModule,
ToDoComponent,
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'tode_project';
}

啟動
1
ng serve
點擊函式觀看 表單與按鈕綁定函式的執行 更正函式

渲染列表 *ngFor

  • 必須引入import { FormsModule } from '@angular/forms'
  • imports: [ CommonModule,// Angular 17以後必須引入 ],
  • *ngFor="let item of posts ; let index = index"
.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
//必須引入
import { FormsModule } from '@angular/forms'
@Component({
selector: 'app-fetch-data',
standalone: true,
imports: [
FormsModule,
CommonModule,// Angular 17以後必須引入
],
templateUrl: './fetch-data.component.html',
styleUrl: './fetch-data.component.scss'
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<ng-container class="container">
<div class="container">
<table class="table table-striped">
<thead>
<tr>
<th scope="col">項目</th>
<th scope="col">姓名</th>
<th scope="col">電子郵件</th>
<th scope="col">城市</th>
<th scope="col">區域</th>

</tr>
</thead>
<tbody>

<!-- @for (post of posts; track post.id) {-->
<tr *ngFor="let item of posts ; let index = index" >
<td>
<p > {{item.user_id}}</p>
</td>
<td>
<input *ngIf="item.isEdit" [(ngModel)]="item.name"/>
<p *ngIf="!item.isEdit"> {{item.name}}</p>
</td>
<td>
<input *ngIf="item.isEdit" [(ngModel)]="item.email"/>
<p *ngIf="!item.isEdit"> {{item.email}}</p>
</td>
<td>
<input *ngIf="item.isEdit" [(ngModel)]="item.country"/>
<p *ngIf="!item.isEdit"> {{item.country}}</p>
</td>

<td>
<input *ngIf="item.isEdit" [(ngModel)]="item.address"/>
<p *ngIf="!item.isEdit"> {{item.address}}</p>
</td>

<td>
<button class="btn btn-warning" (click)="editPost(item.user_id,index)" >Edit</button>
<button class="btn btn-danger ml-2" (click)="remotePost(item.user_id,index)" [ngClass]="{'disabled':item.isEdit}">Delete</button></td>
</tr>
<!-- }-->
</tbody>
</table>
</div>
</ng-container>

判斷是否顯示 *ngIf

#dataEmpty

雙向綁定表單 [(ngModel)]

[ngClass]=”{‘disabled’:item.isEdit}”

1
2
3
4
<input *ngIf="item.isEdit" [(ngModel)]="item.name"/>
<p *ngIf="!item.isEdit"> {{item.name}}</p>

<button class="btn btn-danger ml-2" (click)="remotePost(item.user_id,index)" [ngClass]="{'disabled':item.isEdit}">Delete</button>

Angular CRUD

Angular 表單與按鈕的綁定

表單與按鈕的綁定

to-do.component.html

表單上[(ngModel)]="newTask
按鈕上(click)="addTasks()" 函式的綁定
1
2
3
4
5
6
7
8
<div class="forms">
<div class="input-group">
<input type="text" class="form-control"
[(ngModel)]="newTask"
/>
<button class="btn btn-primary" (click)="addTasks()">Add</button>
</div>
</div>
在to-do.component.ts 如果使用表單綁定 [(ngModel)] 要引入FormsModule
  • 引入FormsModuleimport { FormsModule } from '@angular/forms'
  • importsFormsModule
  • export class ToDoComponent 內的資料定義與函式
    tasks : string[] =[];
    newTask: string = "";//表單綁定
    isAvalible : boolean = false;
    addTasks()函式綁定在Button
    addTasks()函式的邏輯判斷
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { Component } from '@angular/core';
+ import { FormsModule } from '@angular/forms'
@Component({
selector: 'app-to-do',
standalone: true,
imports: [
+ FormsModule
],
templateUrl: './to-do.component.html',
styleUrl: './to-do.component.scss'
})

export class ToDoComponent {
Title: string = 'To Do Lists';
imageLink: string = "https://cdn-icons-png.flaticon.com/512/4697/4697260.png";
+ tasks: string[] = [];
+ newTask: string = "";//表單綁定
isAvalible : boolean = false;
+ addTasks() {
+ const vm = this;
//如果表單左右有空格就清除,將表單數據塞入陣列中,並且將 表單清空
if (vm.newTask.trim() != null) {
vm.tasks.push(vm.newTask)
vm.newTask="";
vm.isAvalible = true;
}
console.log(vm.tasks)
}
}

安裝primeng

PrimeNG 是一套豐富的 Angular 開源 UI 元件。造訪PrimeNG 網站以取得互動式簡報、綜合文件和其他資源。

1
npm install --save primeng 

全局引入 MatButtonModule與ButtonModule

在app.component.ts
  • 引入MatButtonModule
  • 引入ButtonModule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
+ import {MatButtonModule} from '@angular/material/button';
+ import { ButtonModule } from 'primeng/button';
import { ToDoComponent } from './components/to-do/to-do.component';

@Component({
selector: 'app-root',
standalone: true,
imports: [
RouterOutlet,
+ MatButtonModule,
+ ButtonModule,
ToDoComponent,
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'tode_project';
}

啟動
1
ng serve
點擊函式觀看 表單與按鈕綁定函式的執行 更正函式

Angular 資料與圖片的渲染

app/components/to-do/to-do.component.ts
export class ToDoComponent 內新增

Title: string = 'To Do Lists';
imageLink:string= "https://cdn-icons-png.flaticon.com/512/4697/4697260.png";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Component } from '@angular/core';


@Component({
selector: 'app-to-do',
standalone: true,
imports: [],
templateUrl: './to-do.component.html',
styleUrl: './to-do.component.scss'
})
export class ToDoComponent {
Title: string = 'To Do Lists';
imageLink:string= "https://cdn-icons-png.flaticon.com/512/4697/4697260.png";
}
to-do.component.html
資料使用花刮號渲染
圖面使用[src]="定義的圖片位址"
1
2
3
4
5
<div class="title">
<h1 >{{Title}}</h1>
<img [src]="imageLink" alt="">
</div>

Angular Material快速建立各種元件

使用Angular Material來幫我們快速建立各種元件

Angular Material 官網

1
ng add @angular/material

創建to-do

新增components資料夾,右鍵『在整合終端機中開啟』,終端機輸入以下指令
這意味我要新增一個ToDo

1
2
3
ng g c 頁面或是功能
ng g c ToDo

生成的檔案會是『大寫小寫英文-大寫小寫英文.component』副檔名會有

  • .html:HTML檔案
  • .scss:樣式
  • .spec.ts:單元測試檔案
  • .ts:資料定義與函式

components 資料夾內會新增一個 to-do資料夾
資料件內有以下內容

  • to-do.component.html
  • to-do.component.scss
  • to-do.component.spec.ts
  • to-do.component.ts

在app資料夾內app.component.html 新增

1
2
3
4
<main class="main">
<app-to-do></app-to-do>
</main>
<router-outlet />
錯誤訊息:[ERROR] NG8001: 'app-to-do' is not a known element
處理方式:imports引入 ToDoComponent
在app資料夾內引入app.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
+ import { ToDoComponent } from './components/to-do/to-do.component';

@Component({
selector: 'app-root',
standalone: true,
imports: [
RouterOutlet,
+ ToDoComponent
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'tode_project';
}

安裝 Angular Material gitHub

Angular 第一個專案

安裝Angular CLI

node v20.15.1 (npm v10.7.0)
VSCode 擴充套件:Angular Extension Pack
若使用 VSCode(Visual Studio Code)編輯器,可安裝一個叫做 Angular Extension Pack 的擴充套件,整合了 Angular 相關擴充套件方便使用:

1
$ npm i -g @angular/cli@17

查看版本

1
2
ng version
17.3.12

Angular CLI: 17.3.12
Node: 20.15.1
Package Manager: npm 10.7.0
OS: darwin x64

建立Angular專案
1
ng new 專案名稱
Which stylesheet format would you like to use? 您想使用哪一種樣式表格式?Sass (SCSS)
[ https://sass-lang.com/documentation/syntax#scss ] ?
Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? 您想要啟用伺服器端渲染 (SSR) 和靜態網站產生 (SSG/預渲染) 嗎?yes
CREATE test/README.md (1065 bytes)
CREATE test/.editorconfig (274 bytes)
CREATE test/.gitignore (587 bytes)
CREATE test/angular.json (2902 bytes)
CREATE test/package.json (1258 bytes)
CREATE test/tsconfig.json (1012 bytes)
CREATE test/tsconfig.app.json (485 bytes)
CREATE test/tsconfig.spec.json (434 bytes)
CREATE test/server.ts (1729 bytes)
CREATE test/.vscode/extensions.json (130 bytes)
CREATE test/.vscode/launch.json (470 bytes)
CREATE test/.vscode/tasks.json (938 bytes)
CREATE test/src/main.ts (250 bytes)
CREATE test/src/index.html (290 bytes)
CREATE test/src/styles.scss (80 bytes)
CREATE test/src/main.server.ts (264 bytes)
CREATE test/src/app/app.component.scss (0 bytes)
CREATE test/src/app/app.component.html (19903 bytes)
CREATE test/src/app/app.component.spec.ts (910 bytes)
CREATE test/src/app/app.component.ts (301 bytes)
CREATE test/src/app/app.config.ts (404 bytes)
CREATE test/src/app/app.routes.ts (77 bytes)
CREATE test/src/app/app.config.server.ts (350 bytes)
CREATE test/public/favicon.ico (15086 bytes)
✔ Packages installed successfully.
Successfully initialized git.
cd 專案名稱

Would you like to share pseudonymous usage data about this project with the Angular Team at Google under Google's Privacy Policy at https://policies.google.com/privacy. For more details and how to change this setting, see https://angular.dev/cli/analytics.

您願意與 Angular 團隊分享有關此項目的匿名使用數據嗎 根據 Google 的隱私權政策 (https://policies.google.com/privacy) 在 Google 進行操作。了解更多 詳細資訊以及如何更改此設置,請參閱 https://angular.dev/cli/analytics。

1
ng serve --open
這個項目是用生成的 [Angular CLI](https://github.com/angular/angular-cli)
version 18.2.4.

開發啟動

1
ng serve or npm run start

http://localhost:4200/

專案架構

├── node_modules // 安裝的所有套件
├── package.json //整個專案的設定檔,像是應用程式的名稱、版本、描述、關鍵字、授權、貢獻者、維護者、腳本、相依的相關套件及其版本資訊等等,詳細請參考官方文件的說明。
├── package-lock.json //整個專案的設定檔
├── public
│ └── favicon.ico
├── .angular
├── README.md //這個檔案是這個專案的說明文件,採用 Markdown 的語法。可以自由撰寫關於此專案的任何說明。
├── angular.json //Angular CLI 設定檔
├── tsconfig.app.json
├── tsconfig.json // TypeScript 編譯時看的編譯設定檔。
├── tsconfig.spec.json
├── .editorconfig
├── src //主要開發原始碼
├────├── app //包含整個網頁應用程式的 Module、Component、Service
├────├────└── app.routes.ts // 路由
├────├────└── app.config.ts
├────├────└── app.config.server.ts
├────├────└── app.component.ts //
├────├────└── app.component.spec.ts
├────├────└── app.component.scss
├────├────└── app.component.html
├────├────└── home
├────├────└────└── home.component.html //模板 Template
├────├────└────└── home.component.scss //樣式
├────├────└────└── home.component.spec //執行 ng test 命令會透過 Karma 進行測試
├────├────└────└── home.component.ts //元件 Component
├────├────└── page-not-found
├────├────└────└── page-not-found.component.html //模板 Template
├────├────└────└── page-not-found.component.scss //樣式
├────├────└────└── page-not-found.component.spec
├────├────└────└── page-not-found.component.ts
├────├────└── components // 子元件資料夾
├────├────├────└── navbar
├────├────├────└────└────navbar.component.html //模板 Template
├────├────├────└────└────navbar.component.scss //樣式
├────├────├────└────└────navbar.component.spec //執行 ng test 命令會透過 Karma 進行測試
├────├────├────└────└────navbar.component.ts //元件 Component
├────├── environments //環境變數設定檔
├────├────└── environment.prod.ts
├────├────└── environment.ts
├────├── index.html // 起始頁面1-1
├────├── main.server.ts
├────├── main.ts // 進入點載入:Angular CLI 在編譯與打包的時候,把這支檔案裡的程式,當做整個網頁應用程式的主要程式進入點。一般也是不會去動到這裡的程式碼。
├────│── server.ts
├────│── styles.scss

index.html 載入起始頁面1-1

在 index.html 檔案的 標籤內設定了,其定義應用程式執行的根路徑。此標籤是必要的,如果不設定就會讓應用程式找不到網頁,而拋出 404 - Not Found 的錯誤訊息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<title>AngularPermissionsProject</title>
<!------>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

Angular 專案架構

使用Angular Material來幫我們快速建立各種元件

Angular Material快速建立各種元件

產生一個新的組件,先安裝Angular Material

1
2
3
4
5
6
7
8
9
//先安裝Angular Material
ng add @angular/material
套件@angular/material@19.2.1 將被安裝並執行。
您要繼續嗎? (是/否)
The package @angular/material@19.2.1 will be installed and executed.
Would you like to proceed? (Y/n)

//ng g c 頁面或是功能
ng g c ToDo

生成的檔案會是『大寫小寫英文-大寫小寫英文.component』副檔名會有

  • .html:HTML檔案
  • .scss:樣式
  • .spec.ts:單元測試檔案
  • .ts:資料定義與函式

Build 建造

1
ng build

Run ng build 建構項目。建置工件將儲存在 dist/ 目錄。

運行單元測試

Run ng test 透過執行單元測試Karma.

運行端對端測試

Run ng e2e 透過您選擇的平台執行端對端測試。要使用該命令,您需要先新增一個實現端到端測試功能的套件。

Further help

要獲得有關 Angular CLI 使用的更多幫助 ng help 或去看看 Angular CLI Overview and Command Reference page.