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

Quasar Vite Vue3 專案安裝

npm 進行 quasar 建置

使用 npm 進行 quasar 建置。
目前使用 Node v20.17.0

1
npm init quasar

npx
create-quasar

? What would you like to build? › - Use arrow-keys. Return to submit.
? 您想要建造什麼? ›——使用箭頭鍵。返回提交。
❯ App with Quasar CLI, let’s go! <= 選擇這個
❯ 帶有Quasar CLI的應用程序,我們開始吧!
spa/pwa/ssr/bex/electron/capacitor/cordova
AppExtension (AE) for Quasar CLI
Quasar UI kit (Quasar UI 套件)

✔ What would you like to build? › App with Quasar CLI, let’s go!
? Project folder: › quasar-project
? Pick script type: › - Use arrow-keys. Return to submit.
Javascript
❯ Typescript <= 選擇這個
? Pick Quasar App CLI variant: › - Use arrow-keys. Return to submit.
❯ Quasar App CLI with Vite - recommended ( Quasar App CLI 與 Vite - 推薦)<= 選擇這個
Quasar App CLI with Webpack
? Project product name: (must start with letter if building mobile apps) › Quasar App
?專案產品名稱:(如果建立行動應用程序,必須以字母開頭)› Quasar App
? Project description: › A Quasar Project
? Pick a Vue component style: › - Use arrow-keys. Return to submit.
❯ Composition API with < script setup > <= 選擇這個
recommended
Composition API
Options API
? Pick your CSS preprocessor: › - Use arrow-keys. Return to submit.
❯ Sass with SCSS syntax <= 選擇這個
Sass with indented syntax
None (the others will still be available)
Instructions:
↑/↓: Highlight option
←/→/[space]: Toggle selection
a: Toggle all
enter/return: Complete answer
◉ Linting (vite-plugin-checker + ESLint + vue-tsc)
recommended
◉ State Management (Pinia)
◉ axios
◉ vue-i18n

a =>全選
? Add Prettier for code formatting? › (Y/n) ?新增 Prettier 來進行程式碼格式化? › (是/否)<= 選擇是

Quasar • Generating files…

  • README.md
  • .editorconfig
  • .gitignore
  • .npmrc
  • .vscode/extensions.json
  • .vscode/settings.json
  • package.json
  • tsconfig.json
  • index.html
  • postcss.config.js
  • public/favicon.ico
  • public/icons/favicon-128x128.png
  • public/icons/favicon-16x16.png
  • public/icons/favicon-32x32.png
  • public/icons/favicon-96x96.png
  • quasar.config.ts
  • src/App.vue
  • src/assets/quasar-logo-vertical.svg
  • src/boot/.gitkeep
  • src/components/EssentialLink.vue
  • src/components/ExampleComponent.vue
  • src/components/models.ts
  • src/env.d.ts
  • src/layouts/MainLayout.vue
  • src/pages/ErrorNotFound.vue
  • src/pages/IndexPage.vue
  • src/router/index.ts
  • src/router/routes.ts
  • src/css/app.scss
  • src/css/quasar.variables.scss
  • src/boot/axios.ts
  • src/boot/i18n.ts
  • src/i18n/en-US/index.ts
  • src/i18n/index.ts
  • eslint.config.js
  • .prettierrc.json
  • src/stores/example-store.ts
  • src/stores/index.ts

Quasar • SUCCESS • The project has been scaffolded

? Install project dependencies? (recommended) › - Use arrow-keys. Return to submit.
❯ Yes, use npm <= 選擇
No, I will handle that myself

✔ Install project dependencies? (recommended) › Yes, use npm

quasar-project@0.0.1 postinstall
quasar prepare

App • Using quasar.config.ts in “ts” format
App • Generated tsconfig.json and types files in .quasar directory
App • The app is now prepared for linting, type-checking, IDE integration, etc.

added 547 packages, and audited 548 packages in 6m

144 packages are looking for funding
run npm fund for details

found 0 vulnerabilities

Quasar • Initialized Git repository 🚀
Quasar • 初始化 Git 儲存庫
To get started:開始吧:
啟動

1
2
cd quasar-project
npm run dev

Documentation can be found at: https://quasar.dev
文件可在以下網址找到:https://quasar.dev
Quasar is relying on donations to evolve. We’d be very grateful if you can
read our manifest on “Why donations are important”: https://quasar.dev/why-donate
Donation campaign: https://donate.quasar.dev
Any amount is very welcome.
If invoices are required, please first contact Razvan Stoenescu.

Please give us a star on Github if you appreciate our work:
https://github.com/quasarframework/quasar
Quasar 依靠捐贈來發展。如果你能
閱讀我們的「為什麼捐款很重要」宣言:https://quasar.dev/why-donate
捐款活動:https://donate.quasar.dev
無論數量多少,都非常歡迎。
如果需要發票,請先聯絡 Razvan Stoenescu。

如果您欣賞我們的工作,請在 Github 上給我們一顆星:
https://github.com/quasarframework/quasar
Enjoy! - Quasar Team

App • Using quasar.config.ts in “ts” format

» Reported at………… 2025/3/21 下午10:40:30
» App dir……………. /Users/larahuang/Desktop/作品集/天昕科技/quasar-project
» App URL……………. http://localhost:9000/
http://192.168.25.60:9000/
» Dev mode…………… spa
» Pkg quasar…………. v2.18.1
» Pkg @quasar/app-vite… v2.1.4
» Browser target……… es2022|firefox115|chrome115|safari14

App • Opening default browser at http://localhost:9000/

Angular 利用HttpClient取回後端資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import {HttpClient, HttpHeaders } from '@angular/common/http';

@Component({
selector: 'app-test-list',
imports: [NgFor],
templateUrl: './test-list.component.html',
styleUrl: './test-list.component.scss'
})

export class TestListComponent implements OnInit {
getLists() {
const apiUrl ="https://xxxx"
this.http.get<addListsType[]>(`${apiUrl}/api/toDoLists`, this.httpOptions).pipe(catchError(this.handleError))
.subscribe((lists )=> {
consle.log(lists)
this.todoDataList = lists;
})
}
}

上範例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
import {HttpClient, HttpHeaders } from '@angular/common/http';

@Component({
selector: 'app-test-list',
imports: [NgFor],
templateUrl: './test-list.component.html',
styleUrl: './test-list.component.scss'
})

export class TestListComponent implements OnInit {
constructor( private http: HttpClient ) { }
private readonly httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
private handleError(error: any) {
console.error('API 錯誤:', error);
return throwError(() => new Error(error.message || '錯誤已消失'));
}
todoDataList: addListsType[] = [];
tableHeader: string[] = ['項目','名稱','建立日期','更新日期','操作'];
getLists() {
const apiUrl ="https://xxxx"
this.http.get<addListsType[]>(`${apiUrl}/api/toDoLists`, this.httpOptions).pipe(catchError(this.handleError))
.subscribe((lists )=> {
let arrLists:any[] = [];
lists.map( (item: any,index:number) => {
let query: any = {
_id: item._id,
title: item.title,
Editing: false, // 編輯
Status: false, //選取狀態
CanEdit: true, //可以編輯
buildDate: item.buildDate,
updataDate: item.buildDate,
}
// 新增的欄位push 到陣列
arrLists.push(query);

//排序
arrLists.sort((a, b) => {
return b.buildDate - a.buildDate
})
})
//渲染的陣列等於新增後後的陣列
this.todoDataList = arrLists;
})
}
}

html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<table class="table table-striped">
<thead >
<tr >
<th *ngFor="let item of tableHeader| slice: 0 : 5 ;let index = index " scope="col">{{item}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of todoDataList;let index = index">
<td>{{index +1}}</td>
<td>{{item.title | uppercase }} </td>
<td>{{item.buildDate | date:'yyyy-MM-dd HH:mm' }} </td>
<td>{{item.updataDate | TaiwanYear }} </td>

<td>
<button type="button" class="btn btn-secondary">Cancel</button>
<button type="button" class="btn btn-success ml-2">Submit</button></td>
</tr>
</tbody>
</table>


參考

Angular HttpClient 封裝

HttpClient 是 Angular 對 XMLHttpRequest 和 Fetch 的封裝
理解 HttpClient

provideHttpClient 源碼在 provider.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
export function provideHttpClient(
...features: HttpFeature<HttpFeatureKind>[]
): EnvironmentProviders {
if (ngDevMode) {
const featureKinds = new Set(features.map((f) => f.ɵkind));
if (
featureKinds.has(HttpFeatureKind.NoXsrfProtection) &&
featureKinds.has(HttpFeatureKind.CustomXsrfConfiguration)
) {
throw new Error(
ngDevMode
? `Configuration error: found both withXsrfConfiguration() and withNoXsrfProtection() in the same call to provideHttpClient(), which is a contradiction.`
: '',
);
}
}

const providers: Provider[] = [
// 1. HttpClient 是最常用的Service Providers
HttpClient,
HttpXhrBackend,
HttpInterceptorHandler,
{provide: HttpHandler, useExisting: HttpInterceptorHandler},
// 2. default Angular 內部使用XMLHttpRequest發送請求
{provide: HttpBackend, useExisting: HttpXhrBackend},
{
provide: HTTP_INTERCEPTOR_FNS,
useValue: xsrfInterceptorFn,
multi: true,
},
{provide: XSRF_ENABLED, useValue: true},
{provide: HttpXsrfTokenExtractor, useClass: HttpXsrfCookieExtractor},
];

for (const feature of features) {
providers.push(...feature.ɵproviders);
}
// 製作環境提供者
return makeEnvironmentProviders(providers);
}
  • HttpClient 是最常用的Service Providers
  • Angular 默認內部使用XMLHttpRequest發送請求

全局載入

  1. 添加 HttpClient 相關的 providers
    app.config.ts
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
    import { provideRouter } from '@angular/router';
    + import {provideHttpClient} from '@angular/common/http';
    import { routes } from './app.routes';
    import { provideClientHydration, withEventReplay } from '@angular/platform-browser';

    export const appConfig: ApplicationConfig = {
    providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    // 添加 HttpClient 相關的 providers
    + provideHttpClient(),
    provideRouter(routes),
    provideClientHydration(withEventReplay())
    ]
    };
    Fetch 不支持上傳進度,這個是 Fetch 目前最大的缺陷,這也是為什么 Angular 任然以 XMLHttpRequest 作为默認。

src/app 新增 api 資料夾 => 新增 web-api.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
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 || '錯誤已消失'));
}
}

src/app 新增 api 資料夾=> 新增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
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { WebApiService } from './web-api.service';
import { environment } from '../../environments/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> {
console.log('updateTodo', item)
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);
}
}

參考資料

Vite Angular

Vue 安裝
Node v20.17.0
Angular CLI 19.2.5.

1
npm create vite@latest

到專案內

1
cd 專案

Port 4200 is already in use.
Would you like to use a different port? (Y/n)
連接埠 4200 已被使用。
您想使用其他連接埠嗎? (是/否)
Would you like to use a different port? Yes
Component HMR has been enabled.
If you encounter application reload issues, you can manually reload the page to bypass HMR and/or disable this feature with the --no-hmr command line option.
Please consider reporting any issues you encounter here: https://github.com/angular/angular-cli/issues

Browser bundles
Initial chunk files | Names | Raw size
polyfills.js | polyfills | 90.20 kB |
main.js | main | 23.25 kB |
styles.css | styles | 96 bytes |

                 | Initial total    | 113.55 kB

Server bundles
Initial chunk files | Names | Raw size
polyfills.server.mjs | polyfills.server | 570.97 kB |
chunk-VIHJC4M4.mjs | - | 23.69 kB |
server.mjs | server | 1.39 kB |
main.server.mjs | main.server | 800 bytes |

Application bundle generation complete. [2.570 seconds]

Watch mode enabled. Watching for file changes…
NOTE: Raw file sizes do not reflect development server per-request transformations.

參考資料

Angular 19安裝

Angular 分頁套件

Angular 分頁套件

ngx-pagination

1
npm install ngx-pagination --save

ChangeDetectionStrategy 變更偵測策略

Typescript
元件內的變更偵測策略,打造高效能元件
使用 OnPush

https://ithelp.ithome.com.tw/articles/10209130
ChangeDetectionStrategy 變更偵測策略
  • 引入ngx-pagination
  • 裝飾器引入ChangeDetectionStrategy.OnPush
  • 父對子傳值 數據接收
  • 定義分頁:每頁幾筆,當前頁面,全部筆數,選擇變換每頁顯示筆數
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
import {ChangeDetectionStrategy,Component,Input,OnInit,Output,EventEmitter,} from '@angular/core';
import { addListsType } from '../../Types/toDo'
import { NgFor, NgIf, NgClass, CommonModule } from '@angular/common';
+ import {NgxPaginationModule} from 'ngx-pagination';
import { FormsModule } from '@angular/forms';

@Component({
selector: 'app-to-do-list',
imports: [NgxPaginationModule,NgFor,NgIf,NgClass,FormsModule,CommonModule ],
templateUrl: './to-do-list.component.html',
styleUrl: './to-do-list.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush
})

+ export class BasicExampleComponent implements OnInit {
+ @Input() sendTodoList?:addListsType[] |any ;
@Input() sendItemId?: string;
@Input() sendErrorItemMessage?: string;
@Input() sendEditItem?: Observable<addListsType[]>;

@Output() newItem = new EventEmitter<addListsType>();
@Output() newId = new EventEmitter<string>();
@Output() newEvent = new EventEmitter<any>();
@Output() newUpdateItem = new EventEmitter<addListsType>();

openEditItem(item:any) {
this.newItem.emit(item);
}
editItem(item: any) {
this.newUpdateItem.emit(item);
}
openDeleteAlert(id:string) {
this.newId.emit(id)
}
deleteItem(id:string) {
this.newId.emit(id)
}
blurEvent(event:any) {
this.newEvent.emit(event)
}
// 每頁幾筆
+ currentPage: number | undefined = 1;
// 當前頁面
+ itemsPerPage: number = 10;
// 全部筆數
+ totalItems: number | any

+ pagePerOptions = [
+ { value: 5, label: '5筆' },
+ { value: 10, label: '10筆' },
+ { value: 15, label: '15筆' },
+ { value: 20, label: '20筆' },
+ { value: 30, label: '30筆' },
+ { value: 40, label: '40筆' },
+ { value: 50, label: '50筆' },
+ ]
// 選擇變換每頁顯示筆數
+ changeItemsPerPage(item: any) {
+ this.itemsPerPage = item.value;
+ }
// 設備變換時改變每頁顯示
deviceWidth:number =screen.width;
deviceDisplay() {
if (typeof Screen !== undefined) {
switch (this.deviceWidth) {
case 1920:
this.itemsPerPage=20;
break;
case 1560:
this.itemsPerPage=15;
break;
case 1440:
this.itemsPerPage=10;
break;
}
}
}
ngOnInit(): void {
this.deviceDisplay()
}
}

html

  • *ngFor="let item of sendTodoList | paginate: { itemsPerPage: itemsPerPage, currentPage: currentPage} "
  • (pageChange)="currentPage = $event"
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
<div class="lists">
<ul *ngIf="sendTodoList">
<li
*ngFor="let item of sendTodoList |
paginate: { itemsPerPage: itemsPerPage, currentPage: currentPage} ">
<div class="subject">
<label [for]="item._id">
<span class="edit_area"
[ngClass]="sendItemId === item._id ? 'noEditing' : ''"
>
{{item.title}}
</span>
<div class="edit_input_area"
[ngClass]="sendItemId === item._id? 'editing' : ''"
>
<input
type="text"
(blur)="blurEvent($event)"
[(ngModel)]="item.title"
[value]="item.title"
>
<div class="errorMessage">
{{sendErrorItemMessage}}</div>
</div>
</label>
</div>
<div class="buildDate">
{{item.buildDate}}
</div>
<div class="updataDate">
{{item.updataDate}}
</div>
<div class="btns_group">
<button
(click)="openEditItem(item)"
[ngClass]="sendItemId === item._id ? 'noEditing' : ''"
class="btns edit" >
<span>編輯</span><i class="fa-solid fa-pencil"></i>
</button>
<button (click)="editItem(item)"
[ngClass]="sendItemId === item._id? 'editing' : ''"
class="btns add"><span>儲存</span><i class="fa-solid fa-plus"></i></button>
<button
(click)="openDeleteAlert(item._id)"
class="btns delete"><span>刪除</span> <i class="fa-solid fa-xmark"></i></button>
</div>
</li>
</ul>
</div>
<!--分頁細節-->
<div class="pagination_area">
<pagination-controls class="my-pagination" (pageChange)="currentPage = $event"></pagination-controls>
<div class="pagination_detail">
<div class="total_length">
共{{sendTodoList?.length}}筆,</div>
<div class="currentPage"> 當前第{{currentPage}}頁,</div>
<div class="itemsPerPage">每頁{{itemsPerPage}}筆</div>
</div>
<div class="btn-group">
<a class="btn" data-toggle="dropdown" aria-expanded="false">
筆數
</a>
<div class="dropdown-menu">
<a class="dropdown-item"
*ngFor="let item of pagePerOptions"
(click)="changeItemsPerPage(item)"
>{{item.label}}
</a>
</div>
</div>
</div>
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
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
.btns{
display: flex;
height: 30px;
width:70px;
justify-content: center;
align-items: center;
}
.add{
background-color: green;
border: 1px solid green;
color:white;
&:hover,&:focus{
background-color: #0f760f;
border: 1px solid #0f760f;
}
}
.edit{
background-color: #ff9800;
border: 1px solid #ff9800;
color:white;
&:hover,&:focus{
background-color: #bc750c;
border: 1px solid #bc750c;
}
}
.delete{
background-color: red;
border: 1px solid red;
color:white;
&:hover,&:focus{
background-color: rgb(182, 9, 9);
border: 1px solid rgb(182, 9, 9);
}
}
nav{
display: block;
unicode-bidi: isolate;
}
.lists{
display: flex;
justify-content: center;
width: 60%;
max-width: 60%;
margin: auto;
ul{
list-style: none;
padding-inline-start: 0px;
width: 100%;
max-width: 100%;
min-height: 800px;
li{
border-bottom:1px solid #ddd;
display: flex;
list-style: none;
.subject{
align-items: center;
display: flex;
padding: 2px auto;
padding-left: 15px;
width: 50%;
label{
display: flex;
justify-content: flex-start;
width: 95%;
margin-bottom: 0;
span.edit_area{
display:flex;
&.noEditing{
display: none;
}
}
.edit_input_area{
align-items: center;
display: none;
flex-direction: column;
justify-content: flex-start;
width: 100%;
input{
display: flex;
border: 1px dashed #263238;
}
&.editing{
display: flex;
}
}
}

}
.buildDate,.updataDate{
display: flex;
justify-content: center;
align-items: center;
white-space: nowrap;
font-size: 14px;
padding-left: 6px;
}
.btns_group{
display: flex;
}
a,button{
margin: 5px;
cursor: pointer;
white-space: nowrap;
&:disabled {
cursor: not-allowed;
}
i{
margin-left: 5px;
}
&.edit{
display: flex;
&.noEditing{
display: none;
}
}
&.add{
display: none;
&.editing{
display: flex;
}
&:disabled{
cursor: not-allowed;
}
}
}
}
}
}


input{
display: block;
width: 100%;
height: calc(1.2em + .75rem + 2px);
padding: 0.075rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
margin-left: -.75rem;
&:hover{
color: #495057;
background-color: #fff;
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 .2rem rgba(0, 123, 255, .25);
}
}
.errorMessage{
color:red;
font-size: 12px;
align-self: flex-start;
margin-left: -15px;
}



.editing{
display: flex;
}
.noEditing{
display: none;
}
@media screen and (width<=1560px) {
.lists{
ul{
min-height: 480px;
}
}
}
@media screen and (width<=1440px) {
.lists{
ul{
min-height: 480px;
}
}
}
@media screen and (width<=820px){
.add_form,
.lists{
width: 80%;
max-width: 80%;
}
}
//
@media screen and (width<=768px){
.lists {
width: 85%;
max-width: 85%;
ul {
margin-left: 10px;
width: 90%;
li {
margin-left: -15px;
.subject {
width: 45%;
}
}
}
}
}
@media screen and (width<=667px){
.lists {
width: 85%;
max-width: 85%;
ul {
margin-left: 10px;
width: 90%;
li {
margin-left: -15px;
flex-wrap: wrap;
div{
padding: 10px;
}
.subject {
min-width: 100%;
background-color: #f4f3f3;
label{
width: 100%;
height: auto;
.edit_input_area{
&.editing{
flex-direction: none;
padding: 0px;
height: 55px;
}
.errorMessage{
padding: 0;
}
}
}
}
.buildDate,.updataDate{
min-width: 45%;
margin: auto;
}
.btns_group{
margin: auto;
}
}
}
}
}
@media screen and (width<=600px){
.lists ul li a {
span{
display: none;
}
i{
font-size: 1.4rem;
}
&.delete{
i{
font-size: 1.7rem;
}
}
}
}
//iphone5/SE
@media screen and (width<=568px){
.lists {
ul {
li {
.subject {
width: 100%;
max-width: 100%;
white-space:nowrap;

}
.buildDate{
flex-direction: column;
}
}
}
}
}
//iphone 14 pro
@media screen and (width<=430px){
.lists ul li {
margin-left: -15px;
.subject label .edit_area{
font-size: 1.1rem;
}
}
}
@media screen and (width<=414px){
.lists ul li {
margin-left: -15px;
.subject label .edit_area{
font-size: 1.1rem;
}
a{
&.btns{
width: 50px;
}
i{
font-size: 1.4rem;
}
&.delete{
i{
font-size: 1.7rem;
}
}
}
}
}
.pagination_area{
display: flex;
justify-content: center;
.my-pagination ::ng-deep {
align-items: center;
.ngx-pagination {
display: flex;
justify-content: center;
li{
&:first-child{
margin-right: 22px;
}
&.current {
background: transparent;
border-radius: 50%;
width: 32px;
height: 32px;
border: 2px solid #ffd207;
color:#333;
}
&.pagination-previous{
position: relative;
width: 25px;
&.disabled{
color: #ddd;
position: relative;
&:before{
font-size: 42px;
position: absolute;
top: -17px;
left: 0px;
}
span{
display: none;
}
}
a{
position: relative;
font-size: 0px;
&:before{
color: #333;
font-size: 35px;
position: absolute;
top: -11px;
left: -3px;
}
&:hover{
background: transparent;
border:0px solid transparent;
}
}
&:hover{
border-style: transparent;
}
}
a{
&:hover{
font-weight: bolder;
background: transparent;
border-color: #ffd207;
}
}
&.pagination-next {
width: 45px;
&:hover{
border-color: transparent;
}
&::after{
color: #333;
font-size: 35px;
}
&.disabled{
color: #ddd;
position: relative;
&:before{
font-size: 35px;

}
&::after{
color: #ddd;
font-size: 35px;
position: absolute;
top: -11.5px;
left: 17px
}
span{
display: none;
}
}
a{
color: transparent;
font-size: 0px;
position: relative;
&:hover{
font-weight: bolder;
background: transparent;
border-bottom: 1px solid red;
}
&:after{
margin-left: 0rem;
color: #333;
font-size: 35px;
position: absolute;
top: -14px;
left: 20px;
}
&:hover{
background: transparent;
border:0px solid transparent;
}
}
}
}
}
}
.pagination_detail{
display: flex;
margin-top: 4px;
}
.btn-group{
margin-left: 10px;
.btn{
height: 32px;
border: 2px solid #ffd207;
padding: 2px 5px;
&:hover,&:focus{
background-color: #ffd207;
color:#333;
}
}
.dropdown-menu{
min-width: 5rem;
}
}
}

@media screen and (width<=480px){
.pagination_area {
margin-left: -15px;
.pagination_detail{
font-size: 1.1rem;
.currentPage{
display: none;
}
}
}
}
@media screen and (width<=428px){
.pagination_area {
.pagination_detail{
display: none;
}
}
}
@media screen and (width<=360px){
.pagination_area .my-pagination .ngx-pagination {
li{
&:first-child {
margin-right: 5px;
}
&.pagination-previous a{
&:before {
left: 5px;
}
}
&.current{
height: 28px;
width: 28px;
}
}
}

}

例如

新增刪除編輯列表分頁功能

參考資料

Angular 實戰Navbar

功能與邏輯

獲取Nav數據 => 初始化宣染active 在Home => 點擊Nav Item 獲取頁面名稱===pageTitle 新增active樣式
父對子傳值
子元件引入Input
  • @InPut() => import {Input} 引入Input
  • @InPut() 傳值參數:屬性;
  • implements OnInit =>import {OnInit}引入Input OnInit
  • 使用到a標籤時引入RouterLink
  • 使用到動態Class時引入NgClass
從父元件引入子元件
  • 從父元件引入子元件 如果是父對子傳值 [傳值參數]= 父元件內的傳值欄位參數
1
[傳值參數]= 父元件內的傳值欄位參數
子對父傳值
子元件引入Output,EventEmitter
  • Output () 傳值參數名稱 =new EventEmitter<屬性>() ;
  • 函式傳值 this.傳值參數名稱.emit(item)
  • Html =>(傳值參數名稱)="函式($event)"

以下完整程式碼

父元件

從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
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
+import { NavbarComponent } from './components/navbar/navbar.component';
import { CreateNavComponent} from './components/create-nav/create-nav.component';
import { environment } from '../environment/environment'
import { navListType,navListActiveType } from './Types/nav';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
+ imports: [RouterOutlet,NavbarComponent,CreateNavComponent],
styleUrl: './app.component.scss'
})

export class AppComponent {
title = 'vite_project';
navLists: navListType[] = [
{ title: 'Home', path: '/' },
{ title: 'Users', path: 'users' },
{title:'Login',path:'login'}
]
isActive: navListActiveType["isActive"] = true;
pageTitle: navListActiveType["pageTitle"] = "Home";
actionList(title: string) {
this.pageTitle = title;
}
bgStyle:string ='red'
ngOnInit() {
console.log(environment.production ? 'Production' : '開發中')
}
}

app.component.html檔案內引入子元件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<app-navbar
[sendNavLists]="navLists"
[sendPageTitle]="pageTitle"
[sendIsActive]="isActive"
(newPageTitle)="actionList($event)"
></app-navbar>
<app-create-nav
[sendBgStyle]="bgStyle"
[sendCreateLists]="navLists"
[sendPageTitle]="pageTitle"
[sendIsActive]="isActive"
(newPageTitle)="actionList($event)"
></app-create-nav>

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

子元件

navbar.component.ts

  • @InPut() => import {Input} 引入Input
  • @InPut() 傳值參數:屬性;
  • implements OnInit =>import {OnInit}引入Input OnInit
  • 使用到a標籤時引入RouterLink
  • 使用到動態Class時引入NgClass
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 { NgFor,NgClass } from '@angular/common';
+ import { Component,OnInit ,Input,Output, EventEmitter} from '@angular/core';
import { RouterLink } from '@angular/router'; // a標籤 [RouterLink]
import { navListType } from '../../Types/nav'; // Type屬性
//@Component=>裝飾器
@Component({
selector: 'app-navbar',
+ imports: [RouterLink,NgFor,NgClass],
templateUrl: './navbar.component.html',
styleUrl: './navbar.component.scss'
})
// 實現初始化 implements OnInit
+ 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 {}
}
navbar.component.html
  • *ngFor="let item of sendNavLists ; let i = index"
  • [ngClass]="sendPageTitle === item.title ? 'activeNav' : ''"
  • [routerLink]="item.path"
  • (click)="sendActionList(item.title)"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<nav class="nav">
<ul>
<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>
</nav>

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
ul{
display: flex;
li{
list-style: none;
display: flex;
margin: auto 10px;
}
}
.nav {
background-color: #ffd207;
box-shadow: 2px 2px 2px 1px #0003;
display: flex;
height: 70px;
justify-content: center;
list-style-type: none;
margin: 0px auto auto auto;
padding-inline-start: 0px;
max-width: 100%;
width: 100%;

li {
display: flex;
list-style-type: none;
margin: auto 10px;
&.activeNav {
border-bottom: 1px solid #333;
}
a {
cursor: pointer;
color: #333;
font-size: 1.4rem;
text-decoration: none;
}
}
}

.activeNav {
border-bottom: 1px solid #333;
}

github