Angular 表單驗證

input 雙向綁定 [(ngModel)]="form.username" Ts引入 FormsModule

input 綁定 #username=”ngModel”
input 綁定name name="username"
input 綁定required required
input 綁定minlength minlength="ˇ"
input 綁定pattern pattern="^09\d{8}$"

input (change)="onChangeName(username)"
(change)行為函式onChangeName帶入參數username,
console.log(username)
// touched 觸擊
// invalid 無效
// errors 錯誤 [‘required’] 或 [‘minlength’] 或 [‘pattern’]

如果觸擊並且無效 或是 usernameMessage
*ngIf="username.touched && username.invalid || usernameMessage"

form 綁定#loginForm=”ngForm” 點擊 (ngSubmit)=”loginSubmit(loginForm)
< form #loginForm="ngForm" (ngSubmit)="loginSubmit(loginForm)" >
loginSubmit(loginForm) 函式 獲得參數=> 如果不等於空時,跳出驗證

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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
  <div class="form_conatiner">
<form #loginForm="ngForm" (ngSubmit)="loginSubmit(loginForm)">
<div class="mb-3 row">
<!--用戶-->
<label for="username" class="col-sm-2 col-form-label">用戶</label>
<div class="col-sm-10">
<input
#username="ngModel"
[(ngModel)]="form.username"
(change)="onChangeName(username)"
[ngClass]="{'error_border':username.touched && username.errors?.['required'] || username.touched && username.errors?.['minlength'] || usernameMessage }"
minlength="3"
required
type="text"
name="username"
class="form-control" id="username" placeholder="用戶">
<small class="text-danger mt-1"
*ngIf="username.touched && username.invalid || usernameMessage">
<div *ngIf="username.errors?.['required']"> 用戶不能為空!</div>
<div *ngIf="username.errors?.['minlength']"> 用戶不能少於3!</div>
</small>
</div>
</div>
<!--身分證字號-->
<div class="mb-3 row">
<label for="identity" class="col-sm-2 col-form-label">身分證字號</label>
<div class="col-sm-10">
<input
#identity="ngModel"
[(ngModel)]="form.identity"
(change)="onChangeIdentity(identity)"
[ngClass]="{'error_border':identity.touched && identity.errors?.['required'] || identity.errors?.['pattern'] || identityMessage }"
name="identity"
pattern="^[A-Za-z][12]\d{8}$"
required
type="text"
class="form-control"
id="identity"
placeholder="身分證字號"
>
<small class="text-danger mt-1"
*ngIf="identity.touched && identity.invalid || identityMessage">
<div *ngIf="identity.errors?.['required']"> 身分證字號不能為空!</div>
<div *ngIf="identity.errors?.['pattern']"> 身分證字號規格不符!</div>
</small>
</div>
</div>
<!--電子郵件-->
<div class="mb-3 row">
<label for="email" class="col-sm-2 col-form-label">電子郵件</label>
<div class="col-sm-10">
<input
#email="ngModel"
[(ngModel)]="form.email"
(change)="onChangeEmail(email)"
[ngClass]="{'error_border':email.touched && email.errors?.['required'] || email.errors?.['pattern'] || emailMessage }"
name="email"
pattern="^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
required
type="text"
class="form-control"
id="email"
placeholder="電子郵件"
>
<small class="text-danger mt-1"
*ngIf="email.touched && email.invalid || emailMessage">
<div *ngIf="email.errors?.['required']">電子郵件不能為空!</div>
<div *ngIf="email.errors?.['pattern']">電子郵件規格不符!</div>
</small>
</div>
</div>
<!--行動電話-->
<div class="mb-3 row">
<label for="phone" class="col-sm-2 col-form-label">行動電話</label>
<div class="col-sm-10">
<input
#phone="ngModel"
[(ngModel)]="form.phone"
(change)="onChangePhone(phone)"
[ngClass]="{'error_border':phone.touched && phone.errors?.['required'] || phone.errors?.['pattern'] || phoneMessage }"
pattern="^09\d{8}$"
name="phone"
type="text"
required
id="phone"
placeholder="行動電話"
class="form-control">
<small class="text-danger mt-1"
*ngIf="phone.touched && phone.invalid || phoneMessage">
<div *ngIf="phone.errors?.['required']"> 行動電話不能為空!</div>
<div *ngIf="phone.errors?.['pattern']"> 行動電話09開頭規格不符!</div>
</small>
</div>
</div>
<!--密碼-->
<div class="mb-3 row">
<label for="password" class="col-sm-2 col-form-label">密碼</label>
<div class="col-sm-10">
<div class="password">
<input
#password="ngModel"
(change)="onChangePassword(password)"
[ngClass]="{'error_border':password.touched && password.errors?.['required'] || password.touched && password.errors?.['minlength'] || passwordMessage }"
type="password"
[type]="isSeePassword ? 'text' : 'password'"
name="password"
minlength="6"
required
ng-model="inputText"
[(ngModel)]="form.password"
class="form-control"
placeholder="{{passwordPlaceholder}}"
id="password">
<i class="fa-solid fa-eye-slash"
[ngClass]="{'fa-eye':isSeePassword}"
(click)="SeePassword()"></i>
</div>
<small class="text-danger mt-1"
*ngIf="password.touched && password.invalid || passwordMessage">
<div *ngIf="password.errors?.['required']"> 密碼不能為空!</div>
<div *ngIf="password.errors?.['minlength']"> 密碼不能少於6!</div>
</small>
</div>
</div>
<!--驗證碼-->
<div class="mb-3 row verificationCode">
<label for="verificationCode" class="col-sm-2 col-form-label">驗證碼</label>
<div class="col-sm-7">
<input
#verificationCode="ngModel"
[(ngModel)]="form.verificationCode"
(change)="onChangeVerificationCode(verificationCode)"
[ngClass]="{'error_border':
verificationCode.touched && verificationCode.errors?.['required'] ||
form.verificationCode!='' &&newVerificationCode!=form.verificationCode || verificationCodeMessage
}"
minlength="4"
name="verificationCode"
type="text"
required
id="verificationCode"
placeholder="驗證碼"
class="form-control">
<small class="text-danger mt-1"
*ngIf="verificationCode.touched && verificationCode.invalid || verificationCodeMessage" >
<div *ngIf="verificationCode.errors?.['required']">驗證碼不能為空!</div>
<div *ngIf="form.verificationCode!='' &&newVerificationCode!=form.verificationCode">{{verificationCodeValue}}</div>
</small>
</div>
<!--驗證碼顯示-->
<div class="col-sm-2 code_box" (click)="showCode()">
<ul>
<li *ngFor="let i of code_box">
{{ i}}
</li>
</ul>
</div>
<!--點擊產生新的驗證碼-->
<div class="col-sm-1">
<i (click)="showCode()" class="fa-solid fa-arrows-rotate"></i>
</div>
</div>
<div class="btn_group">
<button class="btn btn-outline-secondary" type="submit">重置</button>
<button class="btn btn-outline-primary ml-2"
[disabled]="usernameMessage || identityMessage || emailMessage || phoneMessage|| passwordMessage ||verificationCodeMessage"
type="submit">送出</button>
</div>
</form>
</div>

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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import { NgClass, NgFor, NgIf } from '@angular/common';
import { Component, inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from "@angular/router";
import { HttpProviderService } from '../api/http-provider.service';
interface formType{
username: string | undefined,
identity: string | undefined,
email: string | undefined,
phone: string | undefined,
password: string | undefined,
verificationCode: string | undefined,
}
@Component({
selector: 'app-login',
standalone:true,
imports: [FormsModule,NgFor,NgIf,NgClass],
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
})

export class LoginComponent implements OnInit {
private readonly httpProvider = inject(HttpProviderService);
constructor(private router: Router) {
this.generateCode();
}
form:formType={
username: '',
identity:'',
email: '',
phone:'',
password: '',
verificationCode:''
}
// 是否看見密碼
isSeePassword:boolean = false;
SeePassword() {
this.isSeePassword = !this.isSeePassword;
}
passwordPlaceholder:string='請輸入密碼'
onChangeName(x: { value: string | any[]; touched: any; } ) {
x.value.length===0 || x.touched && x.value.length < 3 ? this.usernameMessage = true:this.usernameMessage = false;
}

onChangeIdentity(x: { value: string | any[]; } ) {
x.value.length===0 ? this.identityMessage = true:this.identityMessage = false;
}

onChangeEmail(x: { value: string | any[]; touched: any; }) {
x.value.length===0 || x.touched && x.value.length < 3 ? this.emailMessage = true:this.emailMessage = false;
}

onChangePhone(x: { value: string | any[]; touched: any; }) {
x.value.length===0 || x.touched && x.value.length < 3 ? this.phoneMessage = true:this.phoneMessage = false;
}

onChangePassword(x: { value: string | any[]; touched: any; }) {
x.value.length===0 || x.touched && x.value.length < 6 ? this.passwordMessage = true:this.passwordMessage = false;
}

verificationCodeValue = '';
onChangeVerificationCode(x: any) {
if (x.value.length===0 || x.touched ) {
this.verificationCodeMessage = true;
}
else if (x.value != '') {
if (this.newVerificationCode != x.value) {
this.verificationCodeValue = '驗證碼不正確!';
this.verificationCodeMessage = true;
} else {
this.verificationCodeMessage = false;
}
}
else {
this.verificationCodeMessage = false;
}
}
usernameMessage: boolean = false;
identityMessage: boolean = false;
emailMessage: boolean = false;
phoneMessage: boolean = false;
passwordMessage: boolean = false;
verificationCodeMessage: boolean = false;
// 用戶的表單規則:
userRule(usernameLength:number) {
switch (true) {
case usernameLength ===0 :
this.usernameMessage = true;
break;
case usernameLength <3:
this.usernameMessage = true;
break;
default:
this.usernameMessage = false;
}
}
// Identity的表單規則:正則表達式不能加引號
regIdentityRulePattern: any = /^[A-Za-z][12]\d{8}$/;
identityRule(identity: string) {
switch (true) {
case identity.length ===0 :
this.identityMessage = true;
break;
case !this.regIdentityRulePattern.test(identity):
this.identityMessage = true;
break;
default:
this.identityMessage = false;
}
}
//電子郵件的表單規則:正則表達式不能加引號
regMailRulePattern:any= /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;
emailRule(email: string) {
switch (true) {
case email.length ===0 :
this.emailMessage = true;
break;
case !this.regMailRulePattern.test(email):
this.emailMessage = true;
break;
default:
this.emailMessage = false;
}
}
//行動電話的表單規則:正則表達式不能加引號
regPhoneRulePattern:any= /^09\d{8}$/;
phoneRule(phone:string) {
switch (true) {
case phone.length ===0 :
this.phoneMessage = true;
break;
case !this.regPhoneRulePattern.test(phone):
this.phoneMessage = true;
break;
default:
this.phoneMessage = false;
}
}
// 密碼的表單規則
passwordRule(passwordLength:number) {
switch (true) {
case passwordLength ===0 :
this.passwordMessage = true;
break;
case passwordLength <6:
this.passwordMessage = true;
break;
default:
this.passwordMessage = false;
}
}
// 驗證碼的表單規則
verificationCodeRule(verificationCode:string) {
switch (true) {
case verificationCode.length ===0 :
this.verificationCodeMessage = true;
break;
case this.verificationCodeValue==='驗證碼不正確!':
this.passwordMessage = true;
break;
default:
this.verificationCodeMessage = false;
}
}

loginSubmit(loginForm: any) {
this.userRule(loginForm.value.username.length)
this.identityRule(loginForm.value.identity)
this.emailRule(loginForm.value.email)
this.phoneRule(loginForm.value.phone)
this.passwordRule(loginForm.value.password.length)
this.verificationCodeRule(loginForm.value.verificationCode)
if (this.emailMessage || this.phoneMessage || this.passwordMessage ) {
console.log('表單漏填');
console.log(this.emailMessage ,this.phoneMessage,this.passwordMessage );
} else {
console.log('loginForm',loginForm)
let query = {
email: loginForm.email,
password: loginForm.password,
}
this.httpProvider.login(query).subscribe({
next: (res: any) => {
localStorage.setItem('token', res.token);
this.router.navigate(["test_login"]);
}
})
}
}

code_box: object | null | undefined | any = [];
newVerificationCode: string = ''
// 產生認證碼
generateCode(length=4) {
let chars = "0123456789";
let code = "";
for (var i = 0; i < length; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
this.newVerificationCode = code;
//驗證碼分割
this.code_box = code.split('');
}
//點擊獲得新的驗證碼
showCode() {
this.generateCode();
}
ngOnInit(): void {
console.log('typeof this.usernameMessage 判斷型別',typeof this.usernameMessage)
}
}
表單驗證github
參考資料

Angular Ng-bootstrap

@ng-bootstrap/ng-bootstrap官網

安裝語法

1
ng add @ng-bootstrap/ng-bootstrap

錯誤訊息 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

angular:styles/global:styles:2:8:
  2 │ @import 'src/styles.scss';
    ╵         ~~~~~~~~~~~~~~~~~
處理方式 angular.json/ "projects": { "vite_angular19_st_project": { "architect": { "build": { "options": { 加在這裡 } } } } }
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
"projects": {
"vite_angular19_st_project": {
"architect": {
"build": {
"options": {
+新增在這裡
"stylePreprocessorOptions": {
"sass": {
"silenceDeprecations": [
"mixed-decls",
"color-functions",
"global-builtin",
"import"
]
}
},

}
}
}
}

新增
"stylePreprocessorOptions": {
"sass": {
"silenceDeprecations": [
"mixed-decls",
"color-functions",
"global-builtin",
"import"
]
}
},

錯誤訊息 [ERROR] Can’t find stylesheet to import.

✘ [ERROR] Can’t find stylesheet to import.

1 │ @use ‘bootstrap/scss/bootstrap’;

處理方式 安裝 bootstrap,如果安裝完成package.json 會出現”dependencies” 內 “bootstrap”: “^5.3.5”,”@popperjs/core”: “^2.11.8”,

1
npm install --save bootstrap

styles.scss引入 bootstrap

1
@use 'bootstrap/scss/bootstrap';

測試Html

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
<ng-template #content let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">Profile update</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="modal.dismiss('Cross click')"></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3">
<label for="dateOfBirth">Date of birth</label>
<div class="input-group">
<input
id="dateOfBirth"
class="form-control"
placeholder="yyyy-mm-dd"
name="dp"
ngbDatepicker
#dp="ngbDatepicker"
/>
<button class="btn btn-outline-secondary bi bi-calendar3" (click)="dp.toggle()" type="button"></button>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="modal.close('Save click')">Save</button>
</div>
</ng-template>

<button class="btn btn-lg btn-outline-primary" (click)="open(content)">Launch demo modal</button>

<hr />

<pre>{{ closeResult() }}</pre>

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
import { Component, inject, signal, TemplateRef, WritableSignal } from '@angular/core';

import { ModalDismissReasons, NgbDatepickerModule, NgbModal } from '@ng-bootstrap/ng-bootstrap';

@Component({
selector: 'ngbd-modal-basic',
imports: [NgbDatepickerModule],
templateUrl: './modal-basic.html',
})
export class NgbdModalBasic {
private modalService = inject(NgbModal);
closeResult: WritableSignal<string> = signal('');

open(content: TemplateRef<any>) {
this.modalService.open(content, { ariaLabelledBy: 'modal-basic-title' }).result.then(
(result) => {
this.closeResult.set(`Closed with: ${result}`);
},
(reason) => {
this.closeResult.set(`Dismissed ${this.getDismissReason(reason)}`);
},
);
}

private getDismissReason(reason: any): string {
switch (reason) {
case ModalDismissReasons.ESC:
return 'by pressing ESC';
case ModalDismissReasons.BACKDROP_CLICK:
return 'by clicking on a backdrop';
default:
return `with: ${reason}`;
}
}
}

Model範例

Angular 打包

Angular 打包

打包指令

1
ng build

angular.json內的設定:

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
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"vite_angular19_todo_project": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
+ "allowedCommonJsDependencies": [
+ "dayjs"
+ ],
+ "outputPath": "dist",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
+ "bootstrap/dist/css/bootstrap.min.css",
"src/styles.scss"
],
"scripts": [
+ "jquery/dist/jquery.min.js",
+ "popper.js/dist/umd/popper.min.js",
+ "bootstrap/dist/js/bootstrap.min.js"
],
"server": "src/main.server.ts",
"prerender": true,
"ssr": {
"entry": "src/server.ts"
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "50MB",
"maximumError": "10MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "50MB",
"maximumError": "10MB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": true,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "vite_angular19_todo_project:build:production"
},
"development": {
"buildTarget": "vite_angular19_todo_project:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": "e07e021f-8e3d-4f1f-9cd5-d9911e112b15"
}
}

允許 CommonJs 依賴項
### 錯誤訊息 打包時 [WARNING] Module 'dayjs' used by 'src/app/home/home.component.ts' is not ESM
處理方式
angular.json 檔案內=>"projects"=> "vite_project" => "architect" => "build" => "options" 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"
+ ],
}
}
}
}
}
}

錯誤訊息 打包時 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"
}
]
}
}
}
}
}
}
}
打包路徑
1
"outputPath": "dist",
打包以後
注意要更改js 與css 的位址改為正確。

✘ [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 Service 獨立管理

RxJS 的 BehaviorSubject

利用 RxJS 的 BehaviorSubject,將原本儲存在各 Component 中的狀態,集中儲存在 Service 中,以方便各階層的元件透過 DI 存取,而不需要透過元件間的互動來傳遞狀態數值。

Api Service 獨立管理

Configures Angular’s HttpClient service to be available for injection.
配置 Angular 的 HttpClient 服務以供注入。
provideHttpClient
src/app/app.config.ts

錯誤訊息:ERROR NullInjectorError: R3InjectorError(Environment Injector)[_ProductsService -> _ApiService -> _HttpClient -> _HttpClient]:
處理方式如下
引入 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)
]
};
新增 api資料夾 http-provider.service.ts 與 web-api.service.ts 官網Injectable 注入
Injectable: 裝飾器將某個類別標記為可用,可以作為依賴項提供和注入。
Observable:
透過網路傳輸來取得,則是屬於「非同步任務」,理由是需要連線到伺服器,使用者的應用程式必須等待伺服器回傳資料。
參考資料
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
//web-api.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class WebApiService {
private readonly http = inject(HttpClient);

private readonly httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};

get(url: string): Observable<any> {
return this.http.get(url, this.httpOptions)
.pipe(catchError(this.handleError));
}

post(url: string, model: any): Observable<any> {
return this.http.post(url, model, this.httpOptions)
.pipe(catchError(this.handleError));
}

put(url: string, model: any): Observable<any> {
return this.http.put(url, model, this.httpOptions)
.pipe(catchError(this.handleError));
}

delete(url: string): Observable<any> {
return this.http.delete(url, this.httpOptions)
.pipe(catchError(this.handleError));
}

private handleError(error: any) {
console.error('API 錯誤:', error);
return throwError(() => new Error(error.message || '錯誤已消失'));
}
}

http-provider.service.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
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { WebApiService } from './web-api.service';
import { environment } from '../../environment/environment';
import dayjs from 'dayjs'

@Injectable({ providedIn: 'root' })

export class HttpProviderService {
private readonly apiUrl = environment.apiUrl;
private readonly webApiService = inject(WebApiService);

getAllLists(): Observable<any> {
return this.webApiService.get(`${this.apiUrl}/api/toDoLists`);
}
// 編輯
deleteTodoById(_id: string): Observable<any> {
return this.webApiService.delete(`${this.apiUrl}/api/toDo/${encodeURIComponent(_id)}`);
}

getTodoById(id: string): Observable<any> {
return this.webApiService.get(`${this.apiUrl}/api/toDo/${encodeURIComponent(id)}`);
}
// 新增
addList(newTask: string): Observable<any> {
let query:any = {
title: newTask,
buildDate: Date.now(),
updataDate: Date.now(),
}
return this.webApiService.post(`${this.apiUrl}/api/toDo`,query);
}

// 編輯
updateTodo(item: any): Observable<any> {
let query:any = {
title: item.title,
buildDate: dayjs(item.buildDate).valueOf(),//改為時間搓
updataDate: Date.now(),
}
return this.webApiService.put(`${this.apiUrl}/api/toDo/${encodeURIComponent(item._id)}`, query);
}
}

頁面的引入

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
+ import { HttpProviderService } from '../api/http-provider.service';
import { addListsType } from '../Types/toDo';
import dayjs from 'dayjs'
@Component({
selector: 'app-home',
imports: [CommonModule,RouterModule,FormsModule,ToDoListsComponent ],
templateUrl: './home.component.html',
styleUrl: './home.component.scss'
})

+ export class HomeComponent implements OnInit {
+ private readonly httpProvider = inject(HttpProviderService);
}
todoLists = <addListsType[] >([]);
//獲取數據
async getAllList() {
this.httpProvider.getAllLists().pipe(
tap(lists => {
let arrLists:any[] = [];
lists.map(function(item: any) {
let query: any = {
_id: item._id,
title: item.title,
Editing: false, // 編輯
Status: false, //選取狀態
CanEdit: true, //可以編輯
buildDate: dayjs(item.buildDate).format('YYYY-MM-DD HH:mm'),
updataDate: dayjs(item.updataDate).format('YYYY-MM-DD HH:mm'),
};
arrLists.push(query);
// 排序
arrLists.sort((a, b) => {
return dayjs(b.buildDate).valueOf() - dayjs(a.buildDate).valueOf()
})

});
this.todoLists = arrLists;
}),
catchError((error: any) => {
console.error("獲取錯誤:", error);
return of([]);
})
).subscribe();

}

使用 BehaviorSubject 的狀態儲存器

Subject
Subject 可以接受 Observer 的訂閱。 也可以用來訂閱其他的 Observable,所以他本身既是 Observable 又是 Observer。

BehaviorSubject 繼承自 Subject具有同樣的功能,然而兩者最主要的差異,在於:

  1. BehaviorSubject 可以接受給定初值,而 Subject 不可以。
  2. Subject 只會在被訂閱之後,而所訂閱的 Observable 有發出新值時,才會做通知。而 BehaviorSubject 會在每一 Observer 訂閱時,對其發出目前已收到的最新值。
也因此 BehaviorSubject 這種類似『狀態暫存』的模式,很適合用來做狀態管理之用。
參考資料

Css nth-child 選擇器

nth-child(even) 雙數

css

1
2
3
4
5
6
7
ul li{
padding:5px;
}
ul li:nth-child(even){
background-color:green;
color:white;
}

Scss

1
2
3
4
5
6
7
8
9
ul {
li{
padding:5px;
&:nth-child(even){
background-color:#ffd207;
color:white;
}
}
}

nth-child(odd) 單數

css

1
2
3
4
5
6
7
ul li{
padding:5px;
}
ul li:nth-child(odd){
background-color:green;
color:white;
}

Scss

1
2
3
4
5
6
7
8
9
ul {
li{
padding:5px;
&:nth-child(odd){
background-color:#ffd207;
color:white;
}
}
}

nth-child(3n) 倍數n

3n 指的是 3 乘 n,而 n 是個由0開始的固定數列 ( 就是 0123456789…n),我們就可以得到下列結果清單

1
2
3
4
5
6
7
8
9
 ul {
li{
padding:5px;
&:nth-child(3n){
background-color:#ffd207;
color:#333;
}
}
}

nth-child(an+b) 倍數an+b

3n+1

  • 3 x 0 + 1 = 1
  • 3 x 1 + 1 = 4
  • 3 x 2 + 1 = 7
  • 3 x 3 + 1 = 10
  • 3 x 4 + 1 = 13
1
2
3
4
5
6
7
8
9
 ul {
li{
padding:5px;
&:nth-child(3n+1){
background-color:#ffd207;
color:#333;
}
}
}

nth-child(an-b) 倍數an-b

3n-1

  • 3 x 0 - 1 = -1
  • 3 x 1 - 1 = 2
  • 3 x 2 - 1 = 5
  • 3 x 3 - 1 = 8
  • 3 x 4 - 1 = 11
1
2
3
4
5
6
7
8
9
 ul {
li{
padding:5px;
&:nth-child(3n-1){
background-color:#ffd207;
color:#333;
}
}
}

Angular 防止冒泡

事件冒泡

  • stopPropagation($event)
  • stopPropagation(event: Event) { event.stopPropagation(); console.log('Div clicked!'); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Component } from '@angular/core';

@Component({
selector: 'app-click-event',
template: `<div (click)="stopPropagation($event)">Click on me!</div>`
})
export class ClickEventComponent {

stopPropagation(event: Event) {
event.stopPropagation();
console.log('Div clicked!');
}
}

參考資料

Angular NgClass & NgStyle

動態綁定Class

在 HTML 元素上新增和刪除 CSS 類別。

1
2
ngClass
string | string[] | Set<string> | { [klass: string]: any; } | null | undefined

CSS 類別根據表達式評估的類型進行如下更新:
expression evaluation =>表達式求值

  • 字串 - 新增字串中列出的 CSS 類別(以空格分隔),
  • 陣列 - 新增聲明為陣列元素的 CSS 類,
  • 物件 - 鍵是 CSS 類,當值中給出的表達式計算結果為真值時,它們會被添加,否則會被刪除
必須引入 NgClass
Ts [ngClass]必須引入NgClass
1
2
<some-element [ngClass]="stringExp|arrayExp|objExp|Set">...</some-element>
<some-element [ngClass]="{'class1 class2 class3' : true}">...</some-element>
對於更簡單的用例,您可以直接使用類別綁定。它不需要導入指令。
1
2
3
4
<some-element [class]="'first second'">...</some-element>
<some-element [class.expanded]="isExpanded">...</some-element>
<some-element [class]="['first', 'second']">...</some-element>
<some-element [class]="{'first': true, 'second': true, 'third': false}">...</some-element>

參考官網NgClass

NgStyle

用於更新包含 HTML 元素的樣式的屬性指令。
設定一個或多個樣式屬性,指定為冒號分隔的鍵值對。關鍵是樣式名稱,帶有可選的 . 後綴(例如 ‘top.px’、’font-style.em’)。該值是一個要評估的表達式。將以給定單位表示的結果非空值指派給給定的樣式屬性。如果評估結果為空,則刪除相應的樣式。
必須引入 NgStyle

Ts [ngStyle]必須引入NgStyle
將包含元素的寬度設定為表達式傳回的像素值。
1
2
3
//[ngStyle]="{'Style':動態欄位}"
[ngStyle]="{'background-color': sendBgStyle}"
<some-element [ngStyle]="{'max-width.px': widthExp}">...</some-element>
將包含元素的寬度設定為表達式傳回的像素值。...
1
<some-element [style]="{'font-style': styleExp}">...</some-element>
NgStyle

Angular Serve

RxJS 與 Angular 訊號的互通

重要提示:RxJS Interop 套件可供開發人員預覽。它已經準備好供您嘗試,但是在穩定之前可能會發生變化。

使用 toSignal 從 RxJs Observable 建立訊號

使用 toSignal 函數建立一個追蹤 Observable 值的訊號。它的行為類似於模板中的非同步管道,但更靈活,可以在應用程式的任何位置使用。
rxjs-interop

使用 toObservable 從訊號建立 RxJS Observable Create an RxJS Observable from a signal with toObservable