Skip to content
This repository was archived by the owner on Nov 12, 2025. It is now read-only.

Commit 1ad5101

Browse files
riccardo-forinadlabrecq
authored andcommitted
fix(list): allow setting a custom trackBy function for the underlying ngFor directive (#435)
* fix(list): allow setting a custom trackBy function for the underlying ngFor directive * Linting * DRY tests setup * Rename trackByFn to trackBy to be consistent with ngFor * Restore basic and compound example, add a new one to showcase trackBy usage * Address PR feedback
1 parent 50292ab commit 1ad5101

File tree

7 files changed

+536
-78
lines changed

7 files changed

+536
-78
lines changed

src/app/list/basic-list/example/list-example.component.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,8 @@ <h4>List Component Example</h4>
1818
<tab heading="Heading" (select)="tabSelected($event)">
1919
<list-heading-example *ngIf="activeTab === 'Heading'"></list-heading-example>
2020
</tab>
21+
<tab heading="Polling" (select)="tabSelected($event)">
22+
<list-polling-example *ngIf="activeTab === 'Polling'"></list-polling-example>
23+
</tab>
2124
</tabset>
2225
</div>

src/app/list/basic-list/example/list-example.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { ListModule } from '../list.module';
1616
import { ListBasicExampleComponent } from './list-basic-example.component';
1717
import { ListCompoundExampleComponent } from './list-compound-example.component';
1818
import { ListHeadingExampleComponent } from './list-heading-example.component';
19+
import { ListPollingExampleComponent } from './list-polling-example.component';
1920
import { ListExampleComponent } from './list-example.component';
2021
import { ListPinExampleComponent } from './list-pin-example.component';
2122
import { NodesContentComponent } from './content/nodes-content.component';
@@ -30,6 +31,7 @@ import { SortArrayPipeModule } from '../../../pipe/sort-array';
3031
ListBasicExampleComponent,
3132
ListCompoundExampleComponent,
3233
ListHeadingExampleComponent,
34+
ListPollingExampleComponent,
3335
ListExampleComponent,
3436
ListPinExampleComponent,
3537
NodesContentComponent
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<div class="padding-15">
2+
<div class="row">
3+
<div class="col-sm-12">
4+
<div class="form-group">
5+
<pfng-list id="myList"
6+
[actionTemplate]="actionTemplate"
7+
[config]="listConfig"
8+
[expandTemplate]="expandTemplate"
9+
[items]="items"
10+
[itemTemplate]="itemTemplate"
11+
[trackBy]="trackByIndex"
12+
(onActionSelect)="handleAction($event, null)"
13+
(onClick)="handleClick($event)"
14+
(onDblClick)="handleDblClick($event)"
15+
(onSelectionChange)="handleSelectionChange($event)">
16+
<ng-template #itemTemplate let-item="item" let-index="index">
17+
<div class="list-pf-left">
18+
<span class="fa {{item.typeIcon}} list-pf-icon list-pf-icon-bordered list-pf-icon-small"></span>
19+
</div>
20+
<div class="list-pf-content-wrapper">
21+
<div class="list-pf-main-content">
22+
<div class="list-pf-title">{{item.name}}</div>
23+
<div class="list-pf-description text-overflow-pf">{{item.address}}</div>
24+
</div>
25+
<div class="list-pf-additional-content">
26+
<div>
27+
<span class="pficon pficon-screen"></span>
28+
<strong>{{item.hostCount}}</strong> Hosts
29+
</div>
30+
<div>
31+
<span class="pficon pficon-cluster"></span>
32+
<strong>{{item.clusterCount}}</strong> Clusters
33+
</div>
34+
<div>
35+
<span class="pficon pficon-container-node"></span>
36+
<strong>{{item.nodeCount}}</strong> Nodes
37+
</div>
38+
<div>
39+
<span class="pficon pficon-image"></span>
40+
<strong>{{item.imageCount}}</strong> Images
41+
</div>
42+
</div>
43+
</div>
44+
</ng-template>
45+
<ng-template #actionTemplate let-item="item" let-index="index">
46+
<pfng-action class="list-pf-actions"
47+
[config]="actionConfig"
48+
(onActionSelect)="handleAction($event, item)">
49+
</pfng-action>
50+
</ng-template>
51+
<ng-template #expandTemplate let-item="item" let-index="index">
52+
<p>This should stay open while the list updates.</p>
53+
<basic-content [item]="item"></basic-content>
54+
</ng-template>
55+
</pfng-list>
56+
</div>
57+
</div>
58+
</div>
59+
<div class="row padding-top-10">
60+
<div class="col-sm-12">
61+
<h4 class="actions-label">Settings</h4>
62+
<hr/>
63+
</div>
64+
</div>
65+
<div class="row">
66+
<div class="col-sm-12">
67+
<form role="form">
68+
<div class="form-group">
69+
<label class="radio-inline">
70+
<input id="selectType1" name="selectType" type="radio"
71+
[(ngModel)]="selectType" value="checkbox" (ngModelChange)="updateSelectionType()">Checkbox
72+
</label>
73+
<label class="radio-inline">
74+
<input id="selectType2" name="selectType" type="radio"
75+
[(ngModel)]="selectType" value="radio" (ngModelChange)="updateSelectionType()">Radio Button
76+
</label>
77+
<label class="radio-inline">
78+
<input id="selectType3" name="selectType" type="radio"
79+
[(ngModel)]="selectType" value="row" (ngModelChange)="updateSelectionType()">Row
80+
</label>
81+
<label class="radio-inline">
82+
<input id="selectType4" name="selectType" type="radio"
83+
[(ngModel)]="selectType" value="none" (ngModelChange)="updateSelectionType()">None
84+
</label>
85+
</div>
86+
</form>
87+
</div>
88+
</div>
89+
<div class="row">
90+
<div class="col-sm-12">
91+
<form role="form">
92+
<div class="form-group">
93+
<label class="checkbox-inline">
94+
<input id="dblClick" name="dblClick" type="checkbox"
95+
[(ngModel)]="listConfig.dblClick"
96+
(ngModelChange)="listConfig.multiSelect = false">Double Click
97+
</label>
98+
<label class="checkbox-inline">
99+
<input id="multiSelect" name="multiSelect" type="checkbox"
100+
[(ngModel)]="listConfig.multiSelect"
101+
[disabled]="listConfig.dblClick">Multi Select
102+
</label>
103+
</div>
104+
</form>
105+
</div>
106+
</div>
107+
<div class="row">
108+
<div class="col-sm-12">
109+
<form role="form">
110+
<div class="form-group">
111+
<label class="checkbox-inline">
112+
<input id="useExpandingRows" name="useExpandingRows" type="checkbox"
113+
[(ngModel)]="listConfig.useExpandItems">Simple Expansion
114+
</label>
115+
<label class="checkbox-inline">
116+
<input id="itemsAvailable" name="itemsAvailable" type="checkbox"
117+
[(ngModel)]="itemsAvailable"
118+
(ngModelChange)="updateItemsAvailable()">Items Available
119+
</label>
120+
</div>
121+
</form>
122+
</div>
123+
</div>
124+
<div class="row">
125+
<div class="col-sm-12">
126+
<button (click)="resetItems()">Reset items</button>
127+
</div>
128+
</div>
129+
<div class="row padding-top-10">
130+
<div class="col-sm-12">
131+
<h4 class="actions-label">Actions</h4>
132+
<hr/>
133+
</div>
134+
</div>
135+
<div class="row">
136+
<div class="col-sm-12">
137+
<textarea rows="3" class="col-sm-12">{{actionsText}}</textarea>
138+
</div>
139+
</div>
140+
<div class="row padding-top-10">
141+
<div class="col-sm-12">
142+
<h4>Code</h4>
143+
<hr/>
144+
</div>
145+
</div>
146+
<div>
147+
<tabset>
148+
<tab heading="api">
149+
<iframe class="demoframe" src="docs/classes/listcomponent.html"></iframe>
150+
</tab>
151+
<tab heading="html">
152+
<include-content src="src/app/list/basic-list/example/list-polling-example.component.html"></include-content>
153+
</tab>
154+
<tab heading="typescript">
155+
<include-content src="src/app/list/basic-list/example/list-polling-example.component.ts"></include-content>
156+
</tab>
157+
</tabset>
158+
</div>
159+
</div>
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import {
2+
Component,
3+
OnInit,
4+
TemplateRef,
5+
ViewEncapsulation
6+
} from '@angular/core';
7+
8+
import { cloneDeep } from 'lodash';
9+
10+
import { Action } from '../../../action/action';
11+
import { ActionConfig } from '../../../action/action-config';
12+
import { EmptyStateConfig } from '../../../empty-state/empty-state-config';
13+
import { ListEvent } from '../../list-event';
14+
import { ListConfig } from '../list-config';
15+
16+
@Component({
17+
encapsulation: ViewEncapsulation.None,
18+
selector: 'list-polling-example',
19+
templateUrl: './list-polling-example.component.html'
20+
})
21+
export class ListPollingExampleComponent implements OnInit {
22+
actionsText: string = '';
23+
allItems: any[];
24+
emptyStateConfig: EmptyStateConfig;
25+
items: any[];
26+
itemsAvailable: boolean = true;
27+
listConfig: ListConfig;
28+
actionConfig: ActionConfig;
29+
selectType: string = 'checkbox';
30+
updateItemsInterval: number;
31+
32+
constructor() {
33+
}
34+
35+
ngOnInit(): void {
36+
this.allItems = [
37+
this.makeRandomItem(),
38+
this.makeRandomItem(),
39+
this.makeRandomItem(),
40+
];
41+
this.allItems[0].expanded = true;
42+
this.items = cloneDeep(this.allItems);
43+
44+
this.emptyStateConfig = {
45+
actions: {
46+
primaryActions: [{
47+
id: 'action1',
48+
title: 'Main Action',
49+
tooltip: 'Start the server'
50+
}],
51+
moreActions: [{
52+
id: 'action2',
53+
title: 'Secondary Action 1',
54+
tooltip: 'Do the first thing'
55+
}, {
56+
id: 'action3',
57+
title: 'Secondary Action 2',
58+
tooltip: 'Do something else'
59+
}, {
60+
id: 'action4',
61+
title: 'Secondary Action 3',
62+
tooltip: 'Do something special'
63+
}]
64+
} as ActionConfig,
65+
iconStyleClass: 'pficon-warning-triangle-o',
66+
title: 'No Items Available',
67+
info: 'This is the Empty State component. The goal of a empty state pattern is to provide a good first ' +
68+
'impression that helps users to achieve their goals. It should be used when a list is empty because no ' +
69+
'objects exists and you want to guide the user to perform specific actions.',
70+
helpLink: {
71+
hypertext: 'List example',
72+
text: 'For more information please see the',
73+
url: '#/list'
74+
}
75+
} as EmptyStateConfig;
76+
77+
this.actionConfig = {
78+
primaryActions: [],
79+
moreActions: [{
80+
id: 'hint',
81+
title: 'This menu should stay open while the list updates',
82+
tooltip: 'This menu should stay open while the list updates'
83+
}],
84+
moreActionsDisabled: false,
85+
moreActionsVisible: true
86+
} as ActionConfig;
87+
88+
this.listConfig = {
89+
dblClick: false,
90+
emptyStateConfig: this.emptyStateConfig,
91+
multiSelect: false,
92+
selectItems: false,
93+
selectionMatchProp: 'name',
94+
showCheckbox: true,
95+
showRadioButton: false,
96+
useExpandItems: true
97+
} as ListConfig;
98+
99+
this.updateItemsInterval = <any>setInterval(() => this.updateItems(), 2500);
100+
}
101+
102+
ngDoCheck(): void {
103+
}
104+
105+
ngOnDestroy(): void {
106+
clearInterval(this.updateItemsInterval);
107+
}
108+
109+
updateItems(): void {
110+
if (this.items.length < 20) {
111+
this.items = [...this.items, this.makeRandomItem()];
112+
}
113+
}
114+
115+
resetItems(): void {
116+
this.items = cloneDeep(this.allItems);
117+
}
118+
119+
makeRandomItem(): any {
120+
return {
121+
name: `Random ${getRandomArbitrary(0, 20)}`,
122+
address: `Some Address ${getRandomArbitrary(1, 100)}`,
123+
city: 'Bedrock',
124+
state: 'Washingstone',
125+
typeIcon: 'fa-plane',
126+
clusterCount: getRandomArbitrary(1, 6),
127+
hostCount: getRandomArbitrary(1, 8),
128+
imageCount: getRandomArbitrary(1, 8),
129+
nodeCount: getRandomArbitrary(1, 10)
130+
};
131+
}
132+
133+
/**
134+
* Get the tracking id to use for each row
135+
*
136+
* @param index The current row index
137+
* @param item The current row item
138+
* @returns number
139+
*/
140+
trackByIndex(index: number, item: any): any {
141+
return index;
142+
}
143+
144+
// Actions
145+
146+
handleAction($event: Action, item: any): void {
147+
if ($event.id === 'start' && item !== null) {
148+
item.started = true;
149+
}
150+
this.actionsText = $event.title + ' selected\r\n' + this.actionsText;
151+
}
152+
153+
handleSelectionChange($event: ListEvent): void {
154+
this.actionsText = $event.selectedItems.length + ' items selected\r\n' + this.actionsText;
155+
}
156+
157+
handleClick($event: ListEvent): void {
158+
this.actionsText = $event.item.name + ' clicked\r\n' + this.actionsText;
159+
}
160+
161+
handleDblClick($event: ListEvent): void {
162+
this.actionsText = $event.item.name + ' double clicked\r\n' + this.actionsText;
163+
}
164+
165+
// Row selection
166+
167+
updateItemsAvailable(): void {
168+
this.items = (this.itemsAvailable) ? cloneDeep(this.allItems) : [];
169+
}
170+
171+
updateSelectionType(): void {
172+
if (this.selectType === 'checkbox') {
173+
this.listConfig.selectItems = false;
174+
this.listConfig.showCheckbox = true;
175+
this.listConfig.showRadioButton = false;
176+
} else if (this.selectType === 'radio') {
177+
this.listConfig.selectItems = false;
178+
this.listConfig.showCheckbox = false;
179+
this.listConfig.showRadioButton = true;
180+
} else if (this.selectType === 'row') {
181+
this.listConfig.selectItems = true;
182+
this.listConfig.showCheckbox = false;
183+
this.listConfig.showRadioButton = false;
184+
} else {
185+
this.listConfig.selectItems = false;
186+
this.listConfig.showCheckbox = false;
187+
this.listConfig.showRadioButton = false;
188+
}
189+
}
190+
}
191+
192+
function getRandomArbitrary(min: number, max: number): number {
193+
return Math.floor(Math.random() * (max - min) + min);
194+
}

src/app/list/basic-list/list.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
<!-- items -->
3535
<div class="list-pf-item {{item?.itemStyleClass}}"
3636
[ngClass]="{'active': item.selected || item.expanded}"
37-
*ngFor="let item of (config.usePinItems ? (items | sortArray: 'showPin': true) : items); let i = index">
37+
*ngFor="let item of (config.usePinItems ? (items | sortArray: 'showPin': true) : items); let i = index; trackBy: trackBy">
3838
<div class="list-pf-container" [id]="getId('item', i)" (click)="toggleExpandItem($event, item)">
3939
<!-- pin -->
4040
<div class="pfng-list-pin-container" *ngIf="config.usePinItems">

0 commit comments

Comments
 (0)