PrimeNG開發兩三事

9 mins.
  1. 1. Message
  2. 2. Menu/Bread
  3. 3. Dialog
    1. 3.1. 包在Component中
    2. 3.2. onHide
  4. 4. FileUpload
  5. 5. DataTable
    1. 5.1. Sort
    2. 5.2. Edit
      1. 5.2.1. 行內編輯
      2. 5.2.2. 編輯highlight
      3. 5.2.3. 目前頁的rowIndex
    3. 5.3. Lazy
    4. 5.4. Selection
    5. 5.5. Scroll
  6. 6. Dropdown
  7. 7. 結論

前陣子公司的一個產品算是告一段落,這次使用PrimeNG作為元件的基底,但是在使用的過程中,也是有遇到一些值得記錄的事項,因此特別寫了這篇。

Message

物件的格式是這樣{severity:string, summary:string, details:string},但每次都要重複寫那些屬性有點多餘,因此我將這個封裝成service,直接提供successerrorinfowarn四個方法。

然後把message的物件用subject傳送,這樣就能在app.component.html直接用async的方式接起來。

另外IE11要特別注意一下,details如果沒給值,畫面上會出現null的字,然後就會被殺了orz
這個issue確定會在5.2.4改掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Injectable } from '@angular/core';
import { Message } from 'primeng/primeng';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class MessageService {
messages$ = new Subject<Message[]>();
constructor() {}

success(content: string) {
this.messages$.next([{ severity: 'success', summary: content, detail: '' }]);
}
error(content: string) {
this.messages$.next([{ severity: 'error', summary: content, detail: '' }]);
}
info(content: string) {
this.messages$.next([{ severity: 'info', summary: content, detail: '' }]);
}
warn(content: string) {
this.messages$.next([{ severity: 'warn', summary: content, detail: '' }]);
}
}
1
<p-growl [value]="msgService.messages$ | async"></p-growl>

Menu/Bread

這邊提供一個小tip,如果說route有很相似的,但在menu/bread上面不想要被同時選取,這時候可以加上routerLinkActiveOptions: { exact: true },這個在angular官網有被提到過。

route的情境像是這樣,沒有加上exact的情況,進入產品頁面的時候,是三個都會被反白,但這基本上不是我們要的效果,因此就要加上,讓他要完整路徑都對才會反白。

1
2
3
4
5
menuList: MenuItem[] = [
{label: '產品', routerLink: ['/product'], routerLinkActiveOptions: { exact: true }},
{label: '特賣', routerLink: ['/product/special'], routerLinkActiveOptions: { exact: true }},
{label: '團購', routerLink: ['/product/together'], routerLinkActiveOptions: { exact: true }}
];

Dialog

包在Component中

如果你也有把dialog整個包進去component的需求,那這個方法就很適合你!

dialog的開啟是透過boolean來控制,當寫成component的時候,參數並不會被改變,這時候dialog就不會再次被開啟,因此我們的好朋友subject出現啦!

首先child component可以這樣寫

1
2
3
<p-dialog [(visible)]="displayFlag" [modal]="true">
show something
</p-dialog>
1
2
3
4
5
6
7
8
9
10
export class YourComponent implements OnInit{
@Input() data: Subject<boolean>;
displayFlag: boolean;

ngOnInit() {
this.data.subscribe(value => {
this.displayFlag = true;
});
}
}

再來parent component的地方,就直接把宣告好的subject放進去,然後每次要開啟就呼叫subject

1
2
<button (click)="open()">open dialog</button>
<app-your [data]="openDialog$"></app-your>
1
2
3
4
5
6
7
export class ParentComponent {
openDialog$ = new Subject<boolean>();
open(){
//這邊傳true或false都不影響開啟,重點是要讓subject有值過去觸發
this.openDialog$.next(true);
}
}

onHide

我們有一個需求是表單填寫的內容放在Dialog中,如果有填寫但最後不儲存,離開前需要提示,像是下面這個範例

1
2
3
4
5
6
7
8
9
<p-dialog [(visible)]="displayDialog" [modal]="true">
<!-- 表單內容 -->
<form [formGroup]="form">
</form>
<p-footer>
<button pButton type="button" class="ui-button-secondary" label="離開" (click)="close()"></button>
<button pButton type="button" class="ui-button-primary" label="儲存" (click)="save()"></button>
</p-footer>
</p-dialog>
1
2
3
4
5
6
7
8
9
10
@Component()
export class MyComponent{
displayDialog: boolean;
form: FormGroup;

close(){
//show confirm
this.displayDialog = false;
}
}

但是右上角有個X,按的時候也視同關閉,因此我們需要加上一個事件onHide來做控制,但是當我們像上面範例,把displayDialog改成false,這時候其實也會觸發onHide的事件,因此就要稍微注意一下順序的控制,才能避免不斷的觸發,大致的作法像是這樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component()
export class MyComponent implements OnInit{
displayDialog: boolean;
form: FormGroup;
valueChange: boolean;

ngOnInit(){
this.form.valueChanges
.subscribe(p => this.valueChange = true);
}

close(){
this.displayDialog = false;
}

hide(){
if(this.valueChange){
// show confirm
} else {
this.displayDialog = false;
}
}
}

若將file之類的元件放在dialog中,開啟的時候會觸發一個dection change的錯誤,但這個其實不會真的影響功能操作,只是會讓你看起來不開心QQ,當初遇到這個問題時還有去開issue只是官方沒人理我TAT

FileUpload

在使用這個元件的時候,因為畫面的關係,所以採用mode="basic",但是這會引發一些狀況,像是maxFileSize上傳的檔案太大應該要提示訊息,但這種模式下不會…TAT

另外,每次上傳完都要將資料清除,不然再次按上傳的時候,會沒辦法選取直接傳前一次檔案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component()
export class MyComponent implements OnInit{
@ViewChild(FileUpload) fileUpload: FileUpload;

ngOnInit(){
}

uploadFile(event) {
this.fileService.uploadFile(this.id, this.type, event.files)
.subscribe(fileIds => {
this.fileUpload.clear();
});
}
}

DataTable

這邊我們用的是DataTable而不是新的TurboTable,就只是因為當時只有一個可以用沒得選XD,這個元件其實蠻多小地方要注意的,很容易萬劫不復阿QQ

官方已經不再維護DataTable這個元件了…請大家轉用TurboTable

Sort

在官方範例中,sort很簡單,就是在column中,加上sortable這個屬性,但如果你有搭配headerColumnGroup,那就要注意一下放的位置,除了在下面的column要放field以外,也要在header中的column加上field,這樣元件才會知道要排序data中的哪個欄位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<p-dataTable [value]="sales">
<p-headerColumnGroup>
<p-row>
<p-column header="Brand" rowspan="2"></p-column>
<p-column header="Sale Rate" colspan="4"></p-column>
</p-row>
<p-row>
<!-- 正確寫法 -->
<p-column header="Last Year" [sortable]="true" field="lastYearSale"></p-column>
<!-- 錯誤寫法 -->
<p-column header="This Year" [sortable]="true"></p-column>
<p-column header="Last Year"></p-column>
<p-column header="This Year"></p-column>
</p-row>
</p-headerColumnGroup>

<p-column field="brand"></p-column>
<p-column field="lastYearSale"></p-column>
<p-column field="thisYearSale"></p-column>
<p-column field="lastYearProfit"></p-column>
<p-column field="thisYearProfit"></p-column>
</p-dataTable>

另外一個要特別注意的點,就是資料的型別要統一,不然排序也會亂掉

這邊提供一個我做出來的範例,可以發現點year那個欄位,VM這筆資料竟然獨立在那邊,其實原因很簡單,資料中有nullundefinenumber三種形態,所以對元件來說會把VM那筆獨立排

1
2
3
4
5
6
7
8
9
10
11
12
[
{"brand": "VW", year: null, "color": "Orange", "vin": "dsad231ff"},
{"brand": "Audi", "color": "Black", "vin": "gwregre345"},
{"brand": "Renault", "year": 2005, "color": "Gray", "vin": "h354htr"},
{"brand": "BMW", "year": 2003, "color": "Blue", "vin": "j6w54qgh"},
{"brand": "Mercedes", "year": 1995, "color": "Orange", "vin": "hrtwy34"},
{"brand": "Volvo", "color": "Black", "vin": "jejtyj"},
{"brand": "Honda", "year": 2012, "color": "Yellow", "vin": "g43gr"},
{"brand": "Jaguar", "color": "Orange", "vin": "greg34"},
{"brand": "Ford", "year": 2000, "color": "Black", "vin": "h54hw5"},
{"brand": "Fiat", "year": 2013, "color": "Red", "vin": "245t2s"}
]

Edit

行內編輯

官方給的範例中,edit的功能只能一個欄位一個欄位的編輯,但如果想要的是整個row一起開放編輯,並且有儲存那些的功能怎麼辦?

這樣的功能我是自己刻,用一個屬性來控制現在是編輯還是檢視

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<p-dataTable [value]="sales" [rowStyleClass]="checkHighlight">
<p-column>
<ng-template pTemplate="body" let-row="rowData">
<button *ngIf="!row.isEdit" (click)="row.isEdit=true">Delete</button>
<button *ngIf="row.isEdit" (click)="save(row)">Save</button>
</ng-template>
</p-column>
<p-column field="brand" header="brand">
<ng-template pTemplate="body" let-col let-row="rowData">
<span *ngIf="!row.isEdit">{{row[col.field]}}</span>
<input type="text" *ngIf="row.isEdit" pInputText [(ngModel)]="row[col.field]">
</ng-template>
</p-column>
</p-dataTable>

編輯highlight

那如果說有需要在編輯的模式中,讓row是呈現被選取的樣式,可以使用rowStyleClass,可以透過這個方法來指定class

1
2
3
4
5
6
@Component()
export class MyComponent {
checkHighlight(row){
return row.isEdit ? 'highlight' : '';
}
}

目前頁的rowIndex

因為我們會有個需求是要新增資料,但如果用splice到第一筆,但是使用者切換到第二頁,會看不到可以編輯的那筆row,翻了文件才發現有這個屬性first,這個的值是這頁的第一筆index

依照剛剛的案例,這時候first會等於10,這時候就可以有兩種作法,第一種是把新的資料直接insert到10的位置,或者是把first改成0,在這邊我是使用第一個方法

一開始要指定first為0,不然會沒資料

Lazy

在一個feature中,我不小心加上了這個設定,然後資料明明已經寫入超過一頁的量,但畫面永遠只顯示一頁(就爆炸了TAT)

去看了官方文件才發現原來是誤會他的功能,這是用來解決大量資料時透過api來取得分頁資料的功能

以這個範例來看,要使用lazy的功能就必須要套用三個屬性/方法lazytotalRecordsonLazyLoad,透過這樣的搭配來取得每頁的資料

1
2
3
4
5
6
7
8
<p-dataTable [value]="cars" [lazy]="true" [rows]="10" [paginator]="true" [rowsPerPageOptions]="[5,10,20]"
[totalRecords]="totalRecords" (onLazyLoad)="loadCarsLazy($event)">
<p-header>List of Cars</p-header>
<p-column field="vin" header="Vin"></p-column>
<p-column field="year" header="Year"></p-column>
<p-column field="brand" header="Brand"></p-column>
<p-column field="color" header="Color"></p-column>
</p-dataTable>

2018/05/23 做後續補充…拖有點久QQ

Selection

同事回報的問題,假設目前selectedList有三筆資料,現在重新取得modelselectedList是不會被清除的,而且還會比對不到任何資料所以就不會被選取,因為這時候的物件和前一次不同,

這個行為我不是很確定算不算問題

1
2
<p-dataTable [value]="model" [(selection)]="selectedList">
</p-dataTable>

Scroll

想要做到固定表頭或是固定前面欄位,可以使用這個功能,但是在設定的時候,發現很容易整個崩壞跑版,尤其是有用到group的功能情況下

目前知道必須要寫死欄位寬高才有辦法比較正常顯示,如果是希望能夠根據資料自動長大的話就…

Dropdown

1
<p-dropdown [options]="reasonList" [autoWidth]="false" placeholder="請選擇" appendTo="body" formControlName="versionReason"></p-dropdown>

如果資料是透過api取得,這時候因為option還沒有任何東西,所以寬度會變得很小要等到點了下拉選單才會變寬,所以要加上autoWidth=false就可以避免寬度自動縮放。

另外,如果是把dropdown放在dialog裡面,而且下拉選單會超過dialog大小,這時候會發現下拉選單被吃掉了!這時候要加上appendTo="body"這個設定,讓下拉選單的z-index提高

結論

其實primeng已經可以符合大部分的使用情境,只是有時候官方文件也許寫的沒有很清楚,那跑去看source code(open source 好處XD)或論壇會比較快

primeng團隊也持續的在開發新元件,也不太需要怕沒有人維護,有什麼好的想法也能提供給他們,一起來讓這個元件更好吧!

讀書會分享時,大家的回響讓我勾起回憶,再補上一些

2018/05/23 再補上一些內容