Angular 實戰Todo

手把手使用Angular,目前版本號是Angular Cli 19.2.0,
Node 安裝 v20.17.0,npm 10.8.2

基礎使用:

  • Router路由
  • 環境變數
  • 使用Angular Material來幫我們快速建立各種元件
  • Angular Binding繫結:
    • 內嵌繫結:使用雙括號將變數宣染
    • 屬性繫結[ ]繫結, [(ngModel)]表單綁定 :
      • styleex.[ngStyle]="{'font-size': 26 + counter + 'px'}";[ngStyle]="{'font-size': 26 + counter + 'px'}";
      • class ex.[ngClass]="{'Hidden':isShowLists}" => [ngClass]="{‘樣式’:動態屬性布林值}"; => 引入 import { NgFor, NgIf,NgClass} from '@angular/common';
      • [id]="item._id"
      • 圖片雙向綁定[src]="item.imgPath"
      • 表單雙向綁定 [(ngModel)]="item.Status" => 引入 import { FormsModule } from '@angular/forms'
    • 事件繫結
      • 點擊事件 (click)="sendEditItem(item)"
      • checkbox表單事件 (change)="isAllSelected()"
      • 鍵盤事件(keyup.enter)="sendAddItem(newTask)"
      • 鍵盤事件(blur)
      • 表單事件(input)
    • 渲染列表 *ngFor => import { NgFor} from '@angular/common';
    • 渲染判斷 *NgIf => import { NgIf} from '@angular/common';
  • 父對子傳遞
  • 子對父傳值

Router路由

src/app/app.routes.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
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home/home.component';
import { UsersComponent } from './users/users.component';
import { UserComponent } from './user/user.component';
import { LoginComponent } from './login/login.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: "login", component: LoginComponent},
{ path: '**', component: PageNotFoundComponent },
];
@NgModule({
imports: [
RouterModule.forRoot(routes)
],
exports: [RouterModule]
})
//https://angular.tw/start/start-routing
export class AppRoutingModule { }

環境變數

環境變數

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

產生一個新的組件

1
ng g c 頁面或是功能

父元件

### app.component.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
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { environment } from '../environments/environment';
import { NavbarComponent } from "./components/navbar/navbar.component";
import { navListType } from "./Type/nav";
@Component({
selector: 'app-root',
imports: [
RouterOutlet,
FormsModule,
NavbarComponent,
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = "我的網站";
navLists: navListType[] = [
{ title: 'Home' ,path:'/'},
{ title: 'Users', path: 'user' },
{title:'Login',path:'login'}
]
pageTitle: string = 'Home';
isActive: boolean = false;
actionList(path: string) {
this.pageTitle = path;
}
ngOnInit(): void {
console.log(environment.production ? 'Production' : '開發中')
}
}

子對父傳遞

父對子傳遞

app.component.html

router-outlet 如同Vue 的router-view
1
2
3
4
5
6
7
8
9
10
<app-navbar
[sendNavLists]="navLists"
[sendPageTitle]="pageTitle"
[sendIsActive]="isActive"
(newPageTitle)="actionList($event)"
></app-navbar>

<main class="main">
<router-outlet />
</main>

父元件

父對子傳值

home.component.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
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { environment } from '../../environments/environment';
import { addListsType } from '../Type/toDo';
import { ToDoHeaderComponent } from '../components/to-do-header/to-do-header.component';
import { ToDOComponent } from '../components/to-do/to-do.component';
@Component({
selector: 'app-home',
imports: [FormsModule,ToDoHeaderComponent,ToDOComponent],
templateUrl: './home.component.html',
styleUrl: './home.component.scss'
})
export class HomeComponent {
newTask: string = '';
header:any= {
'Content-Type': 'application/json',
}
//新增
addItem(newTask: string) {
if (newTask.length>0) {
const vm = this;
const api = `${environment.apiUrl}/api/toDo`;
let query: any = {
title: newTask,
buildDate: Date.now(),
updataDate: Date.now(),
}
console.log('query', query)
fetch(api, { method: 'POST', headers: vm.header, body: JSON.stringify(query) })
.then((res) => res.json())
.then((data) => {
//unshift(data) 新增到第一個
//push(data)新㽪到最後一個
vm.addLists.unshift(data)
})
} else {
alert('沒有填寫新增項目')
}
};

//獲取
addLists: addListsType[] = [];
getAddLists() {
const vm = this;
const api = `${environment.apiUrl}/api/toDoLists`;
fetch(api, { method: 'GET' })
.then((res) => {return res.json();})
.then((res) => {
res.map(function (item: addListsType) {
let query: any = {
_id: item._id,
title: item.title,
Editing: false, // 編輯
Status: false, //選取狀態
CanEdit:true, //可以編輯
buildDate: item.buildDate,
updataDate: item.updataDate,
}
return vm.addLists.push(query);
/*Sort()排列*/
vm.addLists.sort(function (a, b) {
return b.buildDate - a.buildDate
})
return vm.addLists;
})
})
.catch((error) => {
console.log(`Error: ${error}`);
})
}
//刪除
deleteItem(item: any) {
const vm = this;
const api = `${environment.apiUrl}/api/toDo/${item._id}`
fetch(api, {method: 'DELETE',headers: this.header})
.then((res) => res.json())
.then((data) => {
const index = vm.addLists.findIndex(task => task._id === item._id);
vm.addLists.splice(index, 1)
});
}
//編輯
editItem(item: any) {
console.log('editItem', item)
const vm = this;
const api = `${environment.apiUrl}/api/toDo/${item._id}`;
let query: any = {
title: item.title,
buildDate: item.buildDate,
updataDate: Date.now(),
}
fetch(api, {
method: 'PUT',
headers: vm.header,
body: JSON.stringify(query)
})
.then((res) => res.json())
.then((data) => {
if (!item.Editing) {
alert("編輯成功")
}
});
}

//全選
selectAll() {

}
ngOnInit(): void {
this.getAddLists();
}
}

home.component.html

1
2
3
4
5
6
7
8
9
10
<app-to-do-header
(sendNewTask)="addItem($event)"
>
</app-to-do-header>
<app-to-do
[sendAddLists]="addLists"
(sendChangeDeleteItem)="deleteItem($event)"
(sendChangeEditItem)="editItem($event)"
></app-to-do>

子元件

這是NavBar

components/to-do-header

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
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { NgFor } from '@angular/common';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { navListType } from '../../Type/nav';
@Component({
selector: 'app-navbar',
imports: [
NgFor,
CommonModule,
RouterLink
],
templateUrl: './navbar.component.html',
styleUrl: './navbar.component.scss',

})

export class NavbarComponent implements OnInit{
@Input() sendNavLists!: navListType[];
@Input() sendPageTitle!: string;
@Input() sendIsActive!: boolean;

@Output() newPageTitle = new EventEmitter<string>();
//點擊頁面按鈕函式
sendActionList(path: string) {
this.newPageTitle.emit(path);
}
ngOnInit(): void {

}
}
1
2
3
4
5
6
7
8
 <ul  class="nav">
<li *ngFor="let item of sendNavLists ; let i = index"
[ngClass]="sendPageTitle === item.title ? 'activeNav' : ''">
<a [routerLink]="item.path"
(click)="sendActionList(item.title)"
class="cursor-pointer">{{item.title}}</a>
</li>
</ul>

這是新增表單區域

子對父傳遞

父對子傳遞

components/to-do-header ### to-do-header.component.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
import { Component,Input, OnInit,Output, EventEmitter } from '@angular/core';
import { FormsModule } from '@angular/forms'
@Component({
selector: 'app-to-do-header',
standalone: true,
imports: [ FormsModule ],
templateUrl: './to-do-header.component.html',
styleUrl: './to-do-header.component.scss'
})
export class ToDoHeaderComponent implements OnInit {
@Output() sendNewTask = new EventEmitter<string>();
newTask: string = "";
header:any= {
'Content-Type': 'application/json',
}
//新增
sendAddItem(newTask: string) {
const message = '沒有填寫新增項目';
newTask.length>0?this.sendNewTask.emit(newTask):this.messageAlert(message )
}
messageAlert(message:string) {
alert(message)
}
ngOnInit(): void {
}
}
### to-do-header.component.html
1
2
3
4
5
6
7
8
9
10
<div class="todo_lists_header">
<input type="text"
[(ngModel)]="newTask"
[value]="newTask"
[placeholder]="newTask"
(keyup.enter)="sendAddItem(newTask)" >
<a class="button add_button"
(click)="sendAddItem(newTask)">新增</a>
</div>

## 這是ToDo列表 ### to-do.component.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
import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core';
import { FormsModule } from '@angular/forms'
import { NgFor, NgIf,NgClass} from '@angular/common';
import { addListsType } from '../../Type/toDo';
@Component({
selector: 'app-to-do',
imports: [NgFor,NgIf,NgClass,FormsModule],
templateUrl: './to-do.component.html',
styleUrl: './to-do.component.scss'
})

export class ToDOComponent implements OnInit{
@Input() sendAddLists!: addListsType[];

@Output() sendChangeDeleteItem = new EventEmitter<any>();
@Output() sendChangeEditItem = new EventEmitter<Object>()

sendDeleteItem(item: any) {
this.sendChangeDeleteItem.emit(item)
}
sendEditItem(item: any) {
item.Editing = !item.Editing;
this.sendChangeEditItem.emit(item)
};

masterSelected:boolean= false;
constructor(){}

sendAllChecked() {
for (var i = 0; i < this.sendAddLists.length; i++) {
this.sendAddLists[i].Status = this.masterSelected;
}
}
isAllSelected() {
this.masterSelected = this.sendAddLists.every(function(item:any) {
return item.Status == true;
})
}
isShowLists: boolean = false;
sendDeleteAll() {
this.isShowLists = true;
// this.sendAddLists =[]
}
ngOnInit(): void { }
}

to-do.component.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<div class="todo_lists">
<div class="subject"><b>待辦事項</b></div>
<div class="flex-start">
<label class="all" for="all">全部
<input
type="checkbox"
name="all"
id="all"
[(ngModel)]="masterSelected"
(change)="sendAllChecked()"
>
<span class="round-mark"></span>
</label>
<i *ngIf="masterSelected"
(click)="sendDeleteAll()"
class="fa-solid fa-trash"></i>
</div>
<ul class="todo-list"
[ngClass]="{'Hidden':isShowLists}"
>
<!--*ngFor 陣列-->
<li
*ngFor="let item of sendAddLists ; let i = index">
<label class="checkBox"
[for]="item._id">
<span class="edit_area"
[ngClass]="{'noEditing':item.Editing}"
>
{{item.title}}
</span>
<!-- 選取狀態-->
<input
type="checkbox"
(change)="isAllSelected()"
[id]="item._id"
[(ngModel)]="item.Status"
>
<span class="round-mark"></span>
<div class="edit_area">
<input
type="text"
[(ngModel)]="item.title"
[ngClass]="{'editing':item.Editing}"
[value]="item.title">
</div>
</label>
<!--操作-->
<i (click)="sendEditItem(item)" class="fa-solid fa-pencil"></i>
<i (click)="sendDeleteItem(item)" class="fa-solid fa-xmark" ></i>
</li>
</ul>
</div>

Attribute, class, and style bindings and Two-way binding
參考資料