Skip to content

Commit

Permalink
[feat] add trackById trackByProp + tests + docs
Browse files Browse the repository at this point in the history
  • Loading branch information
dmorosinotto committed Sep 16, 2023
1 parent efe357f commit 86c4ffe
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default defineConfig({
{ label: 'ifValidator', link: '/utilities/if-validator' },
{ label: 'call apply Pipes', link: '/utilities/call-apply' },
{ label: 'navigationEnd', link: '/utilities/navigation-end' },
{ label: 'trackByDirectives', link: '/utilities/trackby-id-prop' },
],
},
],
Expand Down
53 changes: 53 additions & 0 deletions docs/src/content/docs/utilities/trackby-id-prop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: trackById trackByProp Directives
description: ngxtension/trackby-id-prop
---

`trackById` and `trackByProp` are simple standalone directives that helps to implement the trackBy in \*ngFor without need to write a custom method inside your component.

```ts
import { TrackByDirectives } from 'ngxtension/trackby-id-prop';
//OR SIMPLY IMPORT AND USE ONE OF THIS
import { NgForTrackByIdDirective, NgForTrackByPropDirective } from 'ngxtension/trackby-id-prop';
```

```
```

## Usage

If the items that you iterate with ngFor has an `id` prop (**case-sentitive**!) than you can simple use `trackById` otherwise you can specify the field to be used to track your items using the other directive: `trackByProp:'PROP_NAME'`

```ts
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TrackByDirectives } from 'ngxtension/trackby-id-prop';

@Component({
selector: 'my-app',
standalone: true,
imports: [TrackByDirectives, CommonModule],
template: `
<ul *ngFor="let item of arr; trackById">
// 👈
<li>{{ item.name }}</li>
</ul>
<p *ngFor="let item of arr; trackByProp: 'name'">// 👈 {{ item.name }} @{{ item.id }}</p>
<div *ngFor="let item of arr; trackByProp: 'other'">
// 👈
<!-- THIS WILL FAIL AND ERROR IF PROP 'other' DOESN'T EXIST IN arr ITEMS -->
{{ item | json }}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
public arr = [
{ id: 1, name: 'foo' /* other: 'A1' */ },
{ id: 2, name: 'bar' /* other: 'B2' */ },
{ id: 3, name: 'baz' /* other: 'C3' */ },
];
}
```
3 changes: 3 additions & 0 deletions libs/ngxtension/trackby-id-prop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ngxtension/trackby-id-prop

Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/trackby-id-prop`.
5 changes: 5 additions & 0 deletions libs/ngxtension/trackby-id-prop/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "src/index.ts"
}
}
33 changes: 33 additions & 0 deletions libs/ngxtension/trackby-id-prop/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "ngxtension/trackby-id-prop",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "libs/ngxtension/trackby-id-prop/src",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["trackby-id-prop"],
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
"libs/ngxtension/trackby-id-prop/**/*.ts",
"libs/ngxtension/trackby-id-prop/**/*.html"
]
}
}
}
}
1 change: 1 addition & 0 deletions libs/ngxtension/trackby-id-prop/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './trackby-id-prop';
46 changes: 46 additions & 0 deletions libs/ngxtension/trackby-id-prop/src/trackby-id-prop.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { NgFor } from '@angular/common';
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TrackByDirectives } from './trackby-id-prop';

describe('TrackByDirectives', () => {
@Component({
standalone: true,
template: `
<i *ngFor="let item of arr; trackById">{{ item.name }}</i>
<p *ngFor="let item of arr; trackByProp: 'name'">{{ item.id }}</p>
`,
imports: [NgFor, TrackByDirectives],
})
class Dummy {
arr = [
{ id: 1, name: 'a' },
{ id: 2, name: 'b' },
{ id: 3, name: 'c' },
];
}

it('given items with id, you can use trackById in *ngFor to render all of them', () => {
const fixture = TestBed.createComponent(Dummy);
fixture.detectChanges();

const items = fixture.debugElement.queryAll(By.css('i'));
expect(items).toHaveLength(3);
items.forEach((item, i) => {
expect(item.nativeElement.textContent).toContain(
String.fromCharCode(97 + i)
);
});
});
it('given items with props, you can use trackByProp in *ngFor to render all of them', () => {
const fixture = TestBed.createComponent(Dummy);
fixture.detectChanges();

const items = fixture.debugElement.queryAll(By.css('p'));
expect(items).toHaveLength(3);
items.forEach((item, i) => {
expect(item.nativeElement.textContent).toContain((i + 1).toString());
});
});
});
42 changes: 42 additions & 0 deletions libs/ngxtension/trackby-id-prop/src/trackby-id-prop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//INSPIRED BY https://medium.com/ngconf/make-trackby-easy-to-use-a3dd5f1f733b
import { NgForOf } from '@angular/common';
import {
Directive,
Input,
Provider,
inject,
type NgIterable,
} from '@angular/core';

@Directive({
selector: '[ngForTrackById]',
standalone: true,
})
export class NgForTrackByIdDirective<T extends { id: string | number }> {
@Input() ngForOf!: NgIterable<T>;
private ngFor = inject(NgForOf<T>, { self: true });

constructor() {
this.ngFor.ngForTrackBy = (index: number, item: T) => item.id;
}
}

@Directive({
selector: '[ngForTrackByProp]',
standalone: true,
})
export class NgForTrackByPropDirective<T> {
@Input() ngForOf!: NgIterable<T>;
private ngFor = inject(NgForOf<T>, { self: true });

@Input()
set ngForTrackByProp(trackByProp: keyof T) {
if (!trackByProp) return; //throw new Error("You must specify trackByProp:'VALID_PROP_NAME'");
this.ngFor.ngForTrackBy = (index: number, item: T) => item[trackByProp];
}
}

export const TrackByDirectives: Provider[] = [
NgForTrackByIdDirective,
NgForTrackByPropDirective,
];
3 changes: 3 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
],
"ngxtension/repeat": ["libs/ngxtension/repeat/src/index.ts"],
"ngxtension/resize": ["libs/ngxtension/resize/src/index.ts"],
"ngxtension/trackby-id-prop": [
"libs/ngxtension/trackby-id-prop/src/index.ts"
],
"plugin": ["libs/plugin/src/index.ts"]
}
},
Expand Down

0 comments on commit 86c4ffe

Please sign in to comment.