From c062613a8e0727a57dc466cf4268bee1c2d4bcbb Mon Sep 17 00:00:00 2001 From: thomas Date: Mon, 2 Oct 2023 21:42:47 +0200 Subject: [PATCH] feat(challenge36): add solution on trackby --- README.md | 2 +- .../performance/ngfor-optimize/.eslintrc.json | 36 +++++++ apps/performance/ngfor-optimize/README.md | 13 +++ .../performance/ngfor-optimize/jest.config.ts | 22 ++++ apps/performance/ngfor-optimize/project.json | 99 ++++++++++++++++++ .../ngfor-optimize/src/app/app.component.ts | 58 ++++++++++ .../ngfor-optimize/src/app/app.config.ts | 6 ++ .../ngfor-optimize/src/app/generateList.ts | 15 +++ .../ngfor-optimize/src/app/list.service.ts | 44 ++++++++ .../src/app/person-list.component.ts | 37 +++++++ .../ngfor-optimize/src/app/person.model.ts | 4 + .../ngfor-optimize/src/assets/.gitkeep | 0 .../ngfor-optimize/src/favicon.ico | Bin 0 -> 15086 bytes .../performance/ngfor-optimize/src/index.html | 13 +++ apps/performance/ngfor-optimize/src/main.ts | 7 ++ .../ngfor-optimize/src/styles.scss | 5 + .../ngfor-optimize/src/test-setup.ts | 2 + .../ngfor-optimize/tailwind.config.js | 14 +++ .../ngfor-optimize/tsconfig.app.json | 10 ++ .../ngfor-optimize/tsconfig.editor.json | 7 ++ apps/performance/ngfor-optimize/tsconfig.json | 32 ++++++ .../ngfor-optimize/tsconfig.spec.json | 15 +++ challenge-number.json | 4 +- .../angular-performance/36-ngfor-optimize.md | 54 ++++++++++ docs/src/content/docs/index.mdx | 4 +- .../app/src/app/app.component.ts__tmpl__ | 2 +- .../cli/src/generators/challenge/generator.ts | 2 +- libs/cli/src/generators/readme/generator.ts | 25 +++-- libs/shared/directives/src/index.ts | 1 + .../directives/src/lib/track-by.directive.ts | 51 +++++++++ 30 files changed, 564 insertions(+), 20 deletions(-) create mode 100644 apps/performance/ngfor-optimize/.eslintrc.json create mode 100644 apps/performance/ngfor-optimize/README.md create mode 100644 apps/performance/ngfor-optimize/jest.config.ts create mode 100644 apps/performance/ngfor-optimize/project.json create mode 100644 apps/performance/ngfor-optimize/src/app/app.component.ts create mode 100644 apps/performance/ngfor-optimize/src/app/app.config.ts create mode 100644 apps/performance/ngfor-optimize/src/app/generateList.ts create mode 100644 apps/performance/ngfor-optimize/src/app/list.service.ts create mode 100644 apps/performance/ngfor-optimize/src/app/person-list.component.ts create mode 100644 apps/performance/ngfor-optimize/src/app/person.model.ts create mode 100644 apps/performance/ngfor-optimize/src/assets/.gitkeep create mode 100644 apps/performance/ngfor-optimize/src/favicon.ico create mode 100644 apps/performance/ngfor-optimize/src/index.html create mode 100644 apps/performance/ngfor-optimize/src/main.ts create mode 100644 apps/performance/ngfor-optimize/src/styles.scss create mode 100644 apps/performance/ngfor-optimize/src/test-setup.ts create mode 100644 apps/performance/ngfor-optimize/tailwind.config.js create mode 100644 apps/performance/ngfor-optimize/tsconfig.app.json create mode 100644 apps/performance/ngfor-optimize/tsconfig.editor.json create mode 100644 apps/performance/ngfor-optimize/tsconfig.json create mode 100644 apps/performance/ngfor-optimize/tsconfig.spec.json create mode 100644 docs/src/content/docs/challenges/angular-performance/36-ngfor-optimize.md create mode 100644 libs/shared/directives/src/lib/track-by.directive.ts diff --git a/README.md b/README.md index a98d91be1..84456c4d7 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ If you would like to propose a challenge, this project is open source, so feel f ## Challenges -Check [all 35 challenges](https://angular-challenges.vercel.app/) +Check [all 36 challenges](https://angular-challenges.vercel.app/) ## Contributors ✨ diff --git a/apps/performance/ngfor-optimize/.eslintrc.json b/apps/performance/ngfor-optimize/.eslintrc.json new file mode 100644 index 000000000..bf8df1428 --- /dev/null +++ b/apps/performance/ngfor-optimize/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "app", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "app", + "style": "kebab-case" + } + ] + }, + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ] + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/apps/performance/ngfor-optimize/README.md b/apps/performance/ngfor-optimize/README.md new file mode 100644 index 000000000..657170429 --- /dev/null +++ b/apps/performance/ngfor-optimize/README.md @@ -0,0 +1,13 @@ +# NgFor Optimization + +> Author: Thomas Laforge + +### Run Application + +```bash +npx nx serve performance-ngfor-optimize +``` + +### Documentation and Instruction + +Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular-performance/36-ngfor-optimize/). diff --git a/apps/performance/ngfor-optimize/jest.config.ts b/apps/performance/ngfor-optimize/jest.config.ts new file mode 100644 index 000000000..5b2ccaee0 --- /dev/null +++ b/apps/performance/ngfor-optimize/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'performance-ngfor-optimize', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/apps/performance/ngfor-optimize', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/apps/performance/ngfor-optimize/project.json b/apps/performance/ngfor-optimize/project.json new file mode 100644 index 000000000..c34d04f5b --- /dev/null +++ b/apps/performance/ngfor-optimize/project.json @@ -0,0 +1,99 @@ +{ + "name": "performance-ngfor-optimize", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "prefix": "app", + "sourceRoot": "apps/performance/ngfor-optimize/src", + "tags": [], + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/apps/performance/ngfor-optimize", + "index": "apps/performance/ngfor-optimize/src/index.html", + "main": "apps/performance/ngfor-optimize/src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "apps/performance/ngfor-optimize/tsconfig.app.json", + "assets": [ + "apps/performance/ngfor-optimize/src/favicon.ico", + "apps/performance/ngfor-optimize/src/assets" + ], + "styles": [ + "apps/performance/ngfor-optimize/src/styles.scss", + "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css" + ], + "scripts": [], + "allowedCommonJsDependencies": ["seedrandom"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "performance-ngfor-optimize:build:production" + }, + "development": { + "browserTarget": "performance-ngfor-optimize:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "executor": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "performance-ngfor-optimize:build" + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": [ + "apps/performance/ngfor-optimize/**/*.ts", + "apps/performance/ngfor-optimize/**/*.html" + ] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "apps/performance/ngfor-optimize/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + } + } +} diff --git a/apps/performance/ngfor-optimize/src/app/app.component.ts b/apps/performance/ngfor-optimize/src/app/app.component.ts new file mode 100644 index 000000000..58d89f4f5 --- /dev/null +++ b/apps/performance/ngfor-optimize/src/app/app.component.ts @@ -0,0 +1,58 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { PersonService } from './list.service'; +import { PersonListComponent } from './person-list.component'; + +@Component({ + standalone: true, + imports: [ + PersonListComponent, + FormsModule, + MatFormFieldModule, + MatInputModule, + ], + providers: [PersonService], + selector: 'app-root', + template: ` +

+ List of Persons +

+ + + + + + + `, + host: { + class: 'flex items-center flex-col gap-5', + }, +}) +export class AppComponent implements OnInit { + readonly personService = inject(PersonService); + readonly persons = this.personService.persons; + + label = ''; + + ngOnInit(): void { + this.personService.loadPersons(); + } + + handleKey(event: any) { + if (event.keyCode === 13) { + this.personService.addPerson(this.label); + this.label = ''; + } + } +} diff --git a/apps/performance/ngfor-optimize/src/app/app.config.ts b/apps/performance/ngfor-optimize/src/app/app.config.ts new file mode 100644 index 000000000..59198e627 --- /dev/null +++ b/apps/performance/ngfor-optimize/src/app/app.config.ts @@ -0,0 +1,6 @@ +import { ApplicationConfig } from '@angular/core'; +import { provideAnimations } from '@angular/platform-browser/animations'; + +export const appConfig: ApplicationConfig = { + providers: [provideAnimations()], +}; diff --git a/apps/performance/ngfor-optimize/src/app/generateList.ts b/apps/performance/ngfor-optimize/src/app/generateList.ts new file mode 100644 index 000000000..41b934e71 --- /dev/null +++ b/apps/performance/ngfor-optimize/src/app/generateList.ts @@ -0,0 +1,15 @@ +import { randEmail, randFirstName } from '@ngneat/falso'; +import { Person } from './person.model'; + +export function generateList() { + const arr: Person[] = []; + + for (let i = 0; i < 50; i++) { + arr.push({ + email: randEmail(), + name: randFirstName(), + }); + } + + return arr; +} diff --git a/apps/performance/ngfor-optimize/src/app/list.service.ts b/apps/performance/ngfor-optimize/src/app/list.service.ts new file mode 100644 index 000000000..e56bddef8 --- /dev/null +++ b/apps/performance/ngfor-optimize/src/app/list.service.ts @@ -0,0 +1,44 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { randEmail, randFirstName } from '@ngneat/falso'; +import { generateList } from './generateList'; +import { Person } from './person.model'; + +@Injectable() +export class PersonService { + private readonly fakeBackend = inject(FakeBackendService); + readonly persons = signal([]); + + loadPersons() { + this.persons.set(generateList()); + } + + deletePerson(email: string) { + this.persons.set( + this.fakeBackend + .returnNewList(this.persons()) + .filter((p) => p.email !== email) + ); + } + + updatePerson(email: string) { + this.persons.set( + this.fakeBackend + .returnNewList(this.persons()) + .map((p) => (p.email === email ? { email, name: randFirstName() } : p)) + ); + } + + addPerson(name: string) { + this.persons.set([ + { email: randEmail(), name }, + ...this.fakeBackend.returnNewList(this.persons()), + ]); + } +} + +@Injectable({ providedIn: 'root' }) +export class FakeBackendService { + returnNewList = (input: Person[]): Person[] => [ + ...input.map((i) => ({ ...i })), + ]; +} diff --git a/apps/performance/ngfor-optimize/src/app/person-list.component.ts b/apps/performance/ngfor-optimize/src/app/person-list.component.ts new file mode 100644 index 000000000..69aee34d8 --- /dev/null +++ b/apps/performance/ngfor-optimize/src/app/person-list.component.ts @@ -0,0 +1,37 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +import { CommonModule } from '@angular/common'; +import { Person } from './person.model'; + +@Component({ + selector: 'app-person-list', + standalone: true, + imports: [CommonModule], + template: ` +
+

{{ person.name }}

+
+ + +
+
+ `, + host: { + class: 'w-full flex flex-col', + }, +}) +export class PersonListComponent { + @Input() persons: Person[] = []; + @Output() delete = new EventEmitter(); + @Output() update = new EventEmitter(); +} diff --git a/apps/performance/ngfor-optimize/src/app/person.model.ts b/apps/performance/ngfor-optimize/src/app/person.model.ts new file mode 100644 index 000000000..0ccffbbf9 --- /dev/null +++ b/apps/performance/ngfor-optimize/src/app/person.model.ts @@ -0,0 +1,4 @@ +export interface Person { + email: string; + name: string; +} diff --git a/apps/performance/ngfor-optimize/src/assets/.gitkeep b/apps/performance/ngfor-optimize/src/assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/performance/ngfor-optimize/src/favicon.ico b/apps/performance/ngfor-optimize/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..317ebcb2336e0833a22dddf0ab287849f26fda57 GIT binary patch literal 15086 zcmeI332;U^%p|z7g|#(P)qFEA@4f!_@qOK2 z_lJl}!lhL!VT_U|uN7%8B2iKH??xhDa;*`g{yjTFWHvXn;2s{4R7kH|pKGdy(7z!K zgftM+Ku7~24TLlh(!g)gz|foI94G^t2^IO$uvX$3(OR0<_5L2sB)lMAMy|+`xodJ{ z_Uh_1m)~h?a;2W{dmhM;u!YGo=)OdmId_B<%^V^{ovI@y`7^g1_V9G}*f# zNzAtvou}I!W1#{M^@ROc(BZ! z+F!!_aR&Px3_reO(EW+TwlW~tv*2zr?iP7(d~a~yA|@*a89IUke+c472NXM0wiX{- zl`UrZC^1XYyf%1u)-Y)jj9;MZ!SLfd2Hl?o|80Su%Z?To_=^g_Jt0oa#CT*tjx>BI z16wec&AOWNK<#i0Qd=1O$fymLRoUR*%;h@*@v7}wApDl^w*h}!sYq%kw+DKDY)@&A z@9$ULEB3qkR#85`lb8#WZw=@})#kQig9oqy^I$dj&k4jU&^2(M3q{n1AKeGUKPFbr z1^<)aH;VsG@J|B&l>UtU#Ejv3GIqERzYgL@UOAWtW<{p#zy`WyJgpCy8$c_e%wYJL zyGHRRx38)HyjU3y{-4z6)pzb>&Q1pR)B&u01F-|&Gx4EZWK$nkUkOI|(D4UHOXg_- zw{OBf!oWQUn)Pe(=f=nt=zkmdjpO^o8ZZ9o_|4tW1ni+Un9iCW47*-ut$KQOww!;u z`0q)$s6IZO!~9$e_P9X!hqLxu`fpcL|2f^I5d4*a@Dq28;@2271v_N+5HqYZ>x;&O z05*7JT)mUe&%S0@UD)@&8SmQrMtsDfZT;fkdA!r(S=}Oz>iP)w=W508=Rc#nNn7ym z1;42c|8($ALY8#a({%1#IXbWn9-Y|0eDY$_L&j{63?{?AH{);EzcqfydD$@-B`Y3<%IIj7S7rK_N}je^=dEk%JQ4c z!tBdTPE3Tse;oYF>cnrapWq*o)m47X1`~6@(!Y29#>-#8zm&LXrXa(3=7Z)ElaQqj z-#0JJy3Fi(C#Rx(`=VXtJ63E2_bZGCz+QRa{W0e2(m3sI?LOcUBx)~^YCqZ{XEPX)C>G>U4tfqeH8L(3|pQR*zbL1 zT9e~4Tb5p9_G}$y4t`i*4t_Mr9QYvL9C&Ah*}t`q*}S+VYh0M6GxTTSXI)hMpMpIq zD1ImYqJLzbj0}~EpE-aH#VCH_udYEW#`P2zYmi&xSPs_{n6tBj=MY|-XrA;SGA_>y zGtU$?HXm$gYj*!N)_nQ59%lQdXtQZS3*#PC-{iB_sm+ytD*7j`D*k(P&IH2GHT}Eh z5697eQECVIGQAUe#eU2I!yI&%0CP#>%6MWV z@zS!p@+Y1i1b^QuuEF*13CuB zu69dve5k7&Wgb+^s|UB08Dr3u`h@yM0NTj4h7MnHo-4@xmyr7(*4$rpPwsCDZ@2be zRz9V^GnV;;?^Lk%ynzq&K(Aix`mWmW`^152Hoy$CTYVehpD-S1-W^#k#{0^L`V6CN+E z!w+xte;2vu4AmVNEFUOBmrBL>6MK@!O2*N|2=d|Y;oN&A&qv=qKn73lDD zI(+oJAdgv>Yr}8(&@ZuAZE%XUXmX(U!N+Z_sjL<1vjy1R+1IeHt`79fnYdOL{$ci7 z%3f0A*;Zt@ED&Gjm|OFTYBDe%bbo*xXAQsFz+Q`fVBH!N2)kaxN8P$c>sp~QXnv>b zwq=W3&Mtmih7xkR$YA)1Yi?avHNR6C99!u6fh=cL|KQ&PwF!n@ud^n(HNIImHD!h87!i*t?G|p0o+eelJ?B@A64_9%SBhNaJ64EvKgD&%LjLCYnNfc; znj?%*p@*?dq#NqcQFmmX($wms@CSAr9#>hUR^=I+=0B)vvGX%T&#h$kmX*s=^M2E!@N9#m?LhMvz}YB+kd zG~mbP|D(;{s_#;hsKK9lbVK&Lo734x7SIFJ9V_}2$@q?zm^7?*XH94w5Qae{7zOMUF z^?%F%)c1Y)Q?Iy?I>knw*8gYW#ok|2gdS=YYZLiD=CW|Nj;n^x!=S#iJ#`~Ld79+xXpVmUK^B(xO_vO!btA9y7w3L3-0j-y4 z?M-V{%z;JI`bk7yFDcP}OcCd*{Q9S5$iGA7*E1@tfkyjAi!;wP^O71cZ^Ep)qrQ)N z#wqw0_HS;T7x3y|`P==i3hEwK%|>fZ)c&@kgKO1~5<5xBSk?iZV?KI6&i72H6S9A* z=U(*e)EqEs?Oc04)V-~K5AUmh|62H4*`UAtItO$O(q5?6jj+K^oD!04r=6#dsxp?~}{`?&sXn#q2 zGuY~7>O2=!u@@Kfu7q=W*4egu@qPMRM>(eyYyaIE<|j%d=iWNdGsx%c!902v#ngNg z@#U-O_4xN$s_9?(`{>{>7~-6FgWpBpqXb`Ydc3OFL#&I}Irse9F_8R@4zSS*Y*o*B zXL?6*Aw!AfkNCgcr#*yj&p3ZDe2y>v$>FUdKIy_2N~}6AbHc7gA3`6$g@1o|dE>vz z4pl(j9;kyMsjaw}lO?(?Xg%4k!5%^t#@5n=WVc&JRa+XT$~#@rldvN3S1rEpU$;XgxVny7mki3 z-Hh|jUCHrUXuLr!)`w>wgO0N%KTB-1di>cj(x3Bav`7v z3G7EIbU$z>`Nad7Rk_&OT-W{;qg)-GXV-aJT#(ozdmnA~Rq3GQ_3mby(>q6Ocb-RgTUhTN)))x>m&eD;$J5Bg zo&DhY36Yg=J=$Z>t}RJ>o|@hAcwWzN#r(WJ52^g$lh^!63@hh+dR$&_dEGu&^CR*< z!oFqSqO@>xZ*nC2oiOd0eS*F^IL~W-rsrO`J`ej{=ou_q^_(<$&-3f^J z&L^MSYWIe{&pYq&9eGaArA~*kA + + + + performance-ngfor-optimize + + + + + + + + diff --git a/apps/performance/ngfor-optimize/src/main.ts b/apps/performance/ngfor-optimize/src/main.ts new file mode 100644 index 000000000..514c89a08 --- /dev/null +++ b/apps/performance/ngfor-optimize/src/main.ts @@ -0,0 +1,7 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig).catch((err) => + console.error(err) +); diff --git a/apps/performance/ngfor-optimize/src/styles.scss b/apps/performance/ngfor-optimize/src/styles.scss new file mode 100644 index 000000000..77e408aa8 --- /dev/null +++ b/apps/performance/ngfor-optimize/src/styles.scss @@ -0,0 +1,5 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* You can add global styles to this file, and also import other style files */ diff --git a/apps/performance/ngfor-optimize/src/test-setup.ts b/apps/performance/ngfor-optimize/src/test-setup.ts new file mode 100644 index 000000000..15de72a3c --- /dev/null +++ b/apps/performance/ngfor-optimize/src/test-setup.ts @@ -0,0 +1,2 @@ +import '@testing-library/jest-dom'; +import 'jest-preset-angular/setup-jest'; diff --git a/apps/performance/ngfor-optimize/tailwind.config.js b/apps/performance/ngfor-optimize/tailwind.config.js new file mode 100644 index 000000000..38183db2c --- /dev/null +++ b/apps/performance/ngfor-optimize/tailwind.config.js @@ -0,0 +1,14 @@ +const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind'); +const { join } = require('path'); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'), + ...createGlobPatternsForDependencies(__dirname), + ], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/apps/performance/ngfor-optimize/tsconfig.app.json b/apps/performance/ngfor-optimize/tsconfig.app.json new file mode 100644 index 000000000..58220429a --- /dev/null +++ b/apps/performance/ngfor-optimize/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"], + "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] +} diff --git a/apps/performance/ngfor-optimize/tsconfig.editor.json b/apps/performance/ngfor-optimize/tsconfig.editor.json new file mode 100644 index 000000000..8ae117d96 --- /dev/null +++ b/apps/performance/ngfor-optimize/tsconfig.editor.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "types": ["jest", "node"] + } +} diff --git a/apps/performance/ngfor-optimize/tsconfig.json b/apps/performance/ngfor-optimize/tsconfig.json new file mode 100644 index 000000000..e85865cf5 --- /dev/null +++ b/apps/performance/ngfor-optimize/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "es2022", + "useDefineForClassFields": false, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + }, + { + "path": "./tsconfig.editor.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/apps/performance/ngfor-optimize/tsconfig.spec.json b/apps/performance/ngfor-optimize/tsconfig.spec.json new file mode 100644 index 000000000..1a4817a7d --- /dev/null +++ b/apps/performance/ngfor-optimize/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node", "@testing-library/jest-dom"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/challenge-number.json b/challenge-number.json index 85b441f6f..a6d987c39 100644 --- a/challenge-number.json +++ b/challenge-number.json @@ -1,6 +1,6 @@ { - "total": 35, - "🟢": 12, + "total": 36, + "🟢": 13, "🟠": 116, "🔴": 207 } diff --git a/docs/src/content/docs/challenges/angular-performance/36-ngfor-optimize.md b/docs/src/content/docs/challenges/angular-performance/36-ngfor-optimize.md new file mode 100644 index 000000000..273259239 --- /dev/null +++ b/docs/src/content/docs/challenges/angular-performance/36-ngfor-optimize.md @@ -0,0 +1,54 @@ +--- +title: 🟢 NgFor Optimization +description: Challenge 36 is about ... +sidebar: + order: 13 + badge: New +--- + +
Challenge #36
+ +## Information + +In this application, we have a list of individuals that we can add, delete or update. If you open the developer Chrome panel by pressing **F12**, go to he source tab, and expand the element to see the list, you will notice that each time, you add, delete or update a list item, the entire DOM elements are destroyed and initialized again. (See video below). + + + +We can also use the Angular DevTool to profile our application and understand what is happening inside our application. I will show you how to do it inside the following video. + + + +:::note +If you don't know how to use it, read [the performance introduction page](/challenges/angular-performance/) first and come back after. +::: + +If you need more information about `NgFor`, I invite you to read the [documentation](https://angular.io/api/common/NgFor) first. + +## Statement + +The goal of this challenge is to understand what is causing this DOM refresh and to solve it. + +--- + +:::note +Start the project by running: `npx nx serve ngfor-optimize`. +::: + +:::tip[Reminder] +Your PR title must start with Answer:36. +::: + + diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index c7c1fe5a5..70e1f8627 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -23,8 +23,8 @@ hero: import { Card, CardGrid } from '@astrojs/starlight/components'; - - This repository gathers 35 Challenges related to Angular, Nx, RxJS, Ngrx and Typescript. + + This repository gathers 36 Challenges related to Angular, Nx, RxJS, Ngrx and Typescript. These challenges resolve around real-life issues or specific features to elevate your skills. diff --git a/libs/cli/src/generators/challenge/files/app/src/app/app.component.ts__tmpl__ b/libs/cli/src/generators/challenge/files/app/src/app/app.component.ts__tmpl__ index 937cefb4d..d977f0989 100644 --- a/libs/cli/src/generators/challenge/files/app/src/app/app.component.ts__tmpl__ +++ b/libs/cli/src/generators/challenge/files/app/src/app/app.component.ts__tmpl__ @@ -3,7 +3,7 @@ import { Component } from '@angular/core'; @Component({ standalone: true, imports: [], - selector: 'lib-root', + selector: 'app-root', template: ``, styles: [''], }) diff --git a/libs/cli/src/generators/challenge/generator.ts b/libs/cli/src/generators/challenge/generator.ts index fa3fe1776..b319f1715 100644 --- a/libs/cli/src/generators/challenge/generator.ts +++ b/libs/cli/src/generators/challenge/generator.ts @@ -51,7 +51,7 @@ export async function challengeGenerator(tree: Tree, options: Schema) { tmpl: '', projectName: names(options.name).name, title: options.title, - challengeNumber, + challengeNumber: challengeNumber + 1, docRepository: options.docRepository, }); diff --git a/libs/cli/src/generators/readme/generator.ts b/libs/cli/src/generators/readme/generator.ts index db2ddc6d1..95f507c26 100644 --- a/libs/cli/src/generators/readme/generator.ts +++ b/libs/cli/src/generators/readme/generator.ts @@ -1,5 +1,4 @@ import { Tree, formatFiles } from '@nx/devkit'; -import { readFile, writeFile } from 'fs/promises'; const README_FILENAME = 'README.md'; const OMIT = ['memoized', 'projection', 'testing-table', 'testing-forms']; @@ -46,10 +45,10 @@ function findHref(href) { async function rewriteFile(tree: Tree, file: string) { console.log('Current file', file); - const buffer = await readFile(file, { encoding: 'utf-8' }); + const buffer = tree.read(file); const regex = new RegExp(/Answer:(\d+)/); - const match = buffer.match(regex); + const match = buffer.toString().match(regex); if (!match) throw new Error('NO MATCH'); @@ -69,19 +68,19 @@ async function rewriteFile(tree: Tree, file: string) { -2 )}/${pathElts.at(-1)}/`; - const doc = await readFile(docFile, { encoding: 'utf-8' }); + const doc = tree.read(docFile); const regexTitle = new RegExp(/title:\s(🟢|🟠|🔴)\s(.+?)\n/); - const matchTitle = doc.match(regexTitle); + const matchTitle = doc.toString().match(regexTitle); const title = matchTitle[2]; const regexCommand = new RegExp(/npx nx serve\s(.+?)`\s/); - const matchCommand = buffer.match(regexCommand); + const matchCommand = buffer.toString().match(regexCommand); let command = ''; if (!matchCommand) { const regexOldCommand = new RegExp(/nx serve\s(.+?)\*/); - command = buffer.match(regexOldCommand)[1]; + command = buffer.toString().match(regexOldCommand)[1]; } else { command = matchCommand[1]; } @@ -103,12 +102,12 @@ npx nx serve ${command} Challenge documentation is [here](${link}). `; - await writeFile(file, finalText, { encoding: 'utf-8' }); + tree.write(file, finalText); ///**** */ const regexHref = new RegExp(/Answer:${number}. } const regexHeader = new RegExp(/([\s\S]*?)\s:::note/); - const header = doc.match(regexHeader)[1]; + const header = doc.toString().match(regexHeader)[1]; console.log('header', header); const regexContent = new RegExp( /Author: Thomas Laforge([\s\S]*?)### Submitting your work/ ); - const matchContent = buffer.match(regexContent); + const matchContent = buffer.toString().match(regexContent); let content = ''; if (!matchContent) { const regexOldContent = new RegExp( /Author: Thomas Laforge([\s\S]*?)## Submitting your work/ ); - content = buffer.match(regexOldContent)[1]; + content = buffer.toString().match(regexOldContent)[1]; } else { content = matchContent[1]; } @@ -184,7 +183,7 @@ ${content} ${footerText} `; - await writeFile(docFile, fullDocText, { encoding: 'utf-8' }); + tree.write(docFile, fullDocText); } export async function readmeGenerator(tree: Tree) { diff --git a/libs/shared/directives/src/index.ts b/libs/shared/directives/src/index.ts index be3fd1e43..fd1b22675 100644 --- a/libs/shared/directives/src/index.ts +++ b/libs/shared/directives/src/index.ts @@ -1 +1,2 @@ export * from './lib/cd-flashing.directive'; +export { NgForTrackByModule } from './lib/track-by.directive'; diff --git a/libs/shared/directives/src/lib/track-by.directive.ts b/libs/shared/directives/src/lib/track-by.directive.ts new file mode 100644 index 000000000..47ba73a22 --- /dev/null +++ b/libs/shared/directives/src/lib/track-by.directive.ts @@ -0,0 +1,51 @@ +/* eslint-disable @angular-eslint/directive-selector */ +import { NgFor, NgForOf } from '@angular/common'; +import { + Directive, + Input, + NgIterable, + NgModule, + Provider, + inject, +} from '@angular/core'; + +@Directive({ + selector: '[ngForTrackByProp]', + standalone: true, +}) +export class NgForTrackByPropDirective { + @Input() ngForOf!: NgIterable; + + @Input() + set ngForTrackByProp(ngForTrackBy: keyof T) { + // setter + this.ngFor.ngForTrackBy = (index: number, item: T) => item[ngForTrackBy]; + } + + private ngFor = inject(NgForOf, { self: true }); +} + +@Directive({ + selector: '[ngForTrackById]', + standalone: true, +}) +export class NgForTrackByIdDirective { + @Input() ngForOf!: NgIterable; // 2 + + private ngFor = inject(NgForOf, { self: true }); // 3 + + constructor() { + this.ngFor.ngForTrackBy = (index: number, item: T) => item.id; // 4 + } +} + +export const NgForTrackByDirective: Provider[] = [ + NgForTrackByIdDirective, + NgForTrackByPropDirective, +]; + +@NgModule({ + imports: [NgFor, NgForTrackByDirective], + exports: [NgFor, NgForTrackByDirective], +}) +export class NgForTrackByModule {}