diff --git a/apps/performance/default-onpush/README.md b/apps/performance/default-onpush/README.md index 07a6e71..8d4b638 100644 --- a/apps/performance/default-onpush/README.md +++ b/apps/performance/default-onpush/README.md @@ -8,13 +8,13 @@ In this series of challenges, you will learn how to optimize and enhance the per The first step is to download the [Angular DevTools Chrome extention](https://chrome.google.com/webstore/detail/angular-devtools/ienfalfjdbdpebioblfackkekamfmbnh) if you haven't already done so. This extension allows you to profile your application and detect performance issues. -In this challenge, we will explore the differences and impacts of using `ChangeDetectionStrategy.Default` versus `ChangeDetectionStrategy.OnPush`. To provide a clearer demonstration, I have added counters to each component and each row in our application. However, in real-world scenarios, you may not have such counters. This is where the Angular DevTool profiler comes to the rescue. +In this challenge, we will explore the differences and impacts of using `ChangeDetectionStrategy.Default` versus `ChangeDetectionStrategy.OnPush`. To provide a clearer demonstration, I have added color enlightment to each component and each row in our application. However, in real-world scenarios, you will not have such visualization. This is where the Angular DevTool profiler comes to the rescue. Start by serving this application by running: `npx nx serve performance-default-onpush` inside your terminal. Then open Chrome DevTool by pressing **F12** and switch to the Angular Tab. From there you can select the Profiler tab as shown below. ![profiler tab](./img/profiler-tab.png 'Profiler tab') -Start profiling your application and type some letters inside the input field. You will notice that each number is increasing and the profiler will show you a lot of change detection cycle. +Start profiling your application and type some letters inside the input field. You will notice that each element of your application will flash at each change detection cycle and the profiler will show you a bar for each change detection cycle. If you click on one of the bars (indicated by the yellow arrow on the picture below), you can see that `PersonListComponent`, `RandomComponent` and all the `MatListItem` are impacted by the change detection cycle, even when we only interact with the input field. diff --git a/apps/performance/default-onpush/project.json b/apps/performance/default-onpush/project.json index 571c029..2b2da32 100644 --- a/apps/performance/default-onpush/project.json +++ b/apps/performance/default-onpush/project.json @@ -23,7 +23,8 @@ "apps/performance/default-onpush/src/styles.scss", "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css" ], - "scripts": [] + "scripts": [], + "allowedCommonJsDependencies": ["seedrandom"] }, "configurations": { "production": { diff --git a/apps/performance/default-onpush/src/app/app.component.ts b/apps/performance/default-onpush/src/app/app.component.ts index ac19efc..1eab3c8 100644 --- a/apps/performance/default-onpush/src/app/app.component.ts +++ b/apps/performance/default-onpush/src/app/app.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; +import { randFirstName } from '@ngneat/falso'; import { PersonListComponent } from './person-list.component'; -import { Persons } from './persons'; import { RandomComponent } from './random.component'; @Component({ @@ -11,12 +11,12 @@ import { RandomComponent } from './random.component';
- - + +
`, }) export class AppComponent { - personList = [...Persons]; - person2List = [...Persons]; + girlList = randFirstName({ gender: 'female', length: 10 }); + boyList = randFirstName({ gender: 'male', length: 10 }); } diff --git a/apps/performance/default-onpush/src/app/person-list.component.ts b/apps/performance/default-onpush/src/app/person-list.component.ts index 1e5d8a6..e2d018b 100644 --- a/apps/performance/default-onpush/src/app/person-list.component.ts +++ b/apps/performance/default-onpush/src/app/person-list.component.ts @@ -1,12 +1,12 @@ import { Component, Input } from '@angular/core'; +import { CDFlashingDirective } from '@angular-challenges/shared/directives'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatChipsModule } from '@angular/material/chips'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatListModule } from '@angular/material/list'; -import { Person } from './person'; @Component({ selector: 'app-person-list', @@ -18,10 +18,11 @@ import { Person } from './person'; MatFormFieldModule, MatInputModule, MatChipsModule, + CDFlashingDirective, ], template: ` -

- {{ title }} +

+ {{ title | titlecase }}

@@ -34,18 +35,18 @@ import { Person } from './person'; -
Empty list
- +
Empty list
+

- {{ item.label }} + {{ name }}

-
- {{ count(item) }} -
- +
`, host: { @@ -53,20 +54,15 @@ import { Person } from './person'; }, }) export class PersonListComponent { - @Input() data: Person[] | null = null; + @Input() names: string[] = []; @Input() title = ''; label = ''; handleKey(event: any) { if (event.keyCode === 13) { - this.data?.unshift({ label: this.label, num: 0 }); + this.names?.unshift(this.label); this.label = ''; } } - - count(person: Person) { - person.num++; - return person.num; - } } diff --git a/apps/performance/default-onpush/src/app/person.ts b/apps/performance/default-onpush/src/app/person.ts deleted file mode 100644 index a656c2c..0000000 --- a/apps/performance/default-onpush/src/app/person.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Person { - label: string; - num: number; -} diff --git a/apps/performance/default-onpush/src/app/persons.ts b/apps/performance/default-onpush/src/app/persons.ts deleted file mode 100644 index 47293a0..0000000 --- a/apps/performance/default-onpush/src/app/persons.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { Person } from './person'; - -export const Persons: Person[] = [ - { - label: 'Nettle', - num: 0, - }, - { - label: 'Rosalie', - num: 0, - }, - { - label: 'Rhona', - num: 0, - }, - { - label: 'Talyah', - num: 0, - }, - { - label: 'Fancie', - num: 0, - }, - { - label: 'Kari', - num: 0, - }, - { - label: 'Caresa', - num: 0, - }, - { - label: 'Gerta', - num: 0, - }, - { - label: 'Modestine', - num: 0, - }, - { - label: 'Emogene', - num: 0, - }, - { - label: 'Henryetta', - num: 0, - }, - { - label: 'Darelle', - num: 0, - }, - { - label: 'Clementine', - num: 0, - }, - { - label: 'Arabella', - num: 0, - }, - { - label: 'Constance', - num: 0, - }, - { - label: 'Josey', - num: 0, - }, - { - label: 'Talia', - num: 0, - }, - { - label: 'Loleta', - num: 0, - }, - { - label: 'Tedda', - num: 0, - }, - { - label: 'Francine', - num: 0, - }, - { - label: 'Kittie', - num: 0, - }, - { - label: 'Merry', - num: 0, - }, - { - label: 'Lexi', - num: 0, - }, - { - label: 'Dorie', - num: 0, - }, - { - label: 'Daron', - num: 0, - }, - { - label: 'Bella', - num: 0, - }, - { - label: 'Ashien', - num: 0, - }, - { - label: 'Hyacinthia', - num: 0, - }, - { - label: 'Beatrix', - num: 0, - }, - { - label: 'Tamiko', - num: 0, - }, - { - label: 'Gusty', - num: 0, - }, - { - label: 'Talyah', - num: 0, - }, - { - label: 'Cynthy', - num: 0, - }, - { - label: 'Trudy', - num: 0, - }, - { - label: 'Katharine', - num: 0, - }, - { - label: 'Kelci', - num: 0, - }, - { - label: 'Allyce', - num: 0, - }, - { - label: 'Dorthea', - num: 0, - }, - { - label: 'Patty', - num: 0, - }, - { - label: 'Fernandina', - num: 0, - }, - { - label: 'Fanya', - num: 0, - }, - { - label: 'Lauralee', - num: 0, - }, - { - label: 'Nanice', - num: 0, - }, - { - label: 'Maureen', - num: 0, - }, - { - label: 'Wilmette', - num: 0, - }, - { - label: 'Ellie', - num: 0, - }, - { - label: 'Maybelle', - num: 0, - }, - { - label: 'Angelina', - num: 0, - }, - { - label: 'Dawn', - num: 0, - }, - { - label: 'Arlene', - num: 0, - }, - { - label: 'Maryanne', - num: 0, - }, - { - label: 'Alyda', - num: 0, - }, - { - label: 'Alisun', - num: 0, - }, - { - label: 'Liana', - num: 0, - }, - { - label: 'Alejandrina', - num: 0, - }, - { - label: 'Costanza', - num: 0, - }, - { - label: 'Wendie', - num: 0, - }, - { - label: 'Rosalinda', - num: 0, - }, - { - label: 'Annabela', - num: 0, - }, - { - label: 'Kelsi', - num: 0, - }, - { - label: 'Morgana', - num: 0, - }, - { - label: 'Gabriel', - num: 0, - }, - { - label: 'Cherianne', - num: 0, - }, - { - label: 'Belia', - num: 0, - }, - { - label: 'Jillayne', - num: 0, - }, - { - label: 'Deerdre', - num: 0, - }, - { - label: 'Tedra', - num: 0, - }, - { - label: 'Reine', - num: 0, - }, - { - label: 'Anett', - num: 0, - }, - { - label: 'Donielle', - num: 0, - }, -]; diff --git a/apps/performance/default-onpush/src/app/random.component.ts b/apps/performance/default-onpush/src/app/random.component.ts index 3fc717c..105f7b9 100644 --- a/apps/performance/default-onpush/src/app/random.component.ts +++ b/apps/performance/default-onpush/src/app/random.component.ts @@ -1,13 +1,10 @@ +import { CDFlashingDirective } from '@angular-challenges/shared/directives'; import { Component } from '@angular/core'; + @Component({ selector: 'app-random', standalone: true, - template: `I do nothing but I'm here: {{ count() }}`, + template: `
I do nothing but I'm here
`, + imports: [CDFlashingDirective], }) -export class RandomComponent { - counter = 0; - - count() { - return this.counter++; - } -} +export class RandomComponent {} diff --git a/apps/performance/default-onpush/src/index.html b/apps/performance/default-onpush/src/index.html index 3941588..dcea384 100644 --- a/apps/performance/default-onpush/src/index.html +++ b/apps/performance/default-onpush/src/index.html @@ -2,7 +2,7 @@ - service-worker + Default OnPush diff --git a/apps/performance/default-onpush/tsconfig.editor.json b/apps/performance/default-onpush/tsconfig.editor.json index 4ee6393..146b6d2 100644 --- a/apps/performance/default-onpush/tsconfig.editor.json +++ b/apps/performance/default-onpush/tsconfig.editor.json @@ -1,6 +1,9 @@ { "extends": "./tsconfig.json", - "include": ["src/**/*.ts"], + "include": [ + "src/**/*.ts", + "../../../libs/shared/directives/src/lib/cd-flashing.directive.ts" + ], "compilerOptions": { "types": [] } diff --git a/libs/shared/directives/.eslintrc.json b/libs/shared/directives/.eslintrc.json new file mode 100644 index 0000000..56c12ac --- /dev/null +++ b/libs/shared/directives/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "lib", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "lib", + "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/libs/shared/directives/README.md b/libs/shared/directives/README.md new file mode 100644 index 0000000..9150af5 --- /dev/null +++ b/libs/shared/directives/README.md @@ -0,0 +1,7 @@ +# shared-directives + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shared-directives` to execute the unit tests. diff --git a/libs/shared/directives/jest.config.ts b/libs/shared/directives/jest.config.ts new file mode 100644 index 0000000..3d1f082 --- /dev/null +++ b/libs/shared/directives/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'shared-directives', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/shared/directives', + 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/libs/shared/directives/ng-package.json b/libs/shared/directives/ng-package.json new file mode 100644 index 0000000..a52bb72 --- /dev/null +++ b/libs/shared/directives/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/shared/directives", + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/shared/directives/package.json b/libs/shared/directives/package.json new file mode 100644 index 0000000..47470dc --- /dev/null +++ b/libs/shared/directives/package.json @@ -0,0 +1,12 @@ +{ + "name": "@angular-challenges/shared/directives", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^16.1.0", + "@angular/core": "^16.1.0" + }, + "dependencies": { + "tslib": "^2.3.0" + }, + "sideEffects": false +} diff --git a/libs/shared/directives/project.json b/libs/shared/directives/project.json new file mode 100644 index 0000000..ff976c2 --- /dev/null +++ b/libs/shared/directives/project.json @@ -0,0 +1,50 @@ +{ + "name": "shared-directives", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/shared/directives/src", + "prefix": "lib", + "tags": [], + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/angular:ng-packagr-lite", + "outputs": ["{workspaceRoot}/dist/{projectRoot}"], + "options": { + "project": "libs/shared/directives/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "libs/shared/directives/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "libs/shared/directives/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/shared/directives/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": [ + "libs/shared/directives/**/*.ts", + "libs/shared/directives/**/*.html" + ] + } + } + } +} diff --git a/libs/shared/directives/src/index.ts b/libs/shared/directives/src/index.ts new file mode 100644 index 0000000..be3fd1e --- /dev/null +++ b/libs/shared/directives/src/index.ts @@ -0,0 +1 @@ +export * from './lib/cd-flashing.directive'; diff --git a/libs/shared/directives/src/lib/cd-flashing.directive.ts b/libs/shared/directives/src/lib/cd-flashing.directive.ts new file mode 100644 index 0000000..1811d15 --- /dev/null +++ b/libs/shared/directives/src/lib/cd-flashing.directive.ts @@ -0,0 +1,23 @@ +/* eslint-disable @angular-eslint/directive-selector */ +import { Directive, DoCheck, ElementRef, NgZone } from '@angular/core'; + +@Directive({ + selector: '[cd-flash]', + standalone: true, +}) +export class CDFlashingDirective implements DoCheck { + constructor(private elementRef: ElementRef, private zone: NgZone) {} + + ngDoCheck(): void { + this.cdRan(); + } + + public cdRan(): void { + this.zone.runOutsideAngular(() => { + this.elementRef.nativeElement.classList.add('!bg-orange-500'); + setTimeout(() => { + this.elementRef.nativeElement.classList.remove('!bg-orange-500'); + }, 1000); + }); + } +} diff --git a/libs/shared/directives/src/test-setup.ts b/libs/shared/directives/src/test-setup.ts new file mode 100644 index 0000000..ab1eeeb --- /dev/null +++ b/libs/shared/directives/src/test-setup.ts @@ -0,0 +1,8 @@ +// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment +globalThis.ngJest = { + testEnvironmentOptions: { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, + }, +}; +import 'jest-preset-angular/setup-jest'; diff --git a/libs/shared/directives/tsconfig.json b/libs/shared/directives/tsconfig.json new file mode 100644 index 0000000..5cf0a16 --- /dev/null +++ b/libs/shared/directives/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es2022", + "useDefineForClassFields": false, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/libs/shared/directives/tsconfig.lib.json b/libs/shared/directives/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/libs/shared/directives/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/libs/shared/directives/tsconfig.lib.prod.json b/libs/shared/directives/tsconfig.lib.prod.json new file mode 100644 index 0000000..61b5237 --- /dev/null +++ b/libs/shared/directives/tsconfig.lib.prod.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": {} +} diff --git a/libs/shared/directives/tsconfig.spec.json b/libs/shared/directives/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/libs/shared/directives/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index f432fe9..0d18e9a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -63,6 +63,9 @@ "@angular-challenges/ngrx-notification/model": [ "libs/ngrx-notification/model/src/index.ts" ], + "@angular-challenges/shared/directives": [ + "libs/shared/directives/src/index.ts" + ], "@angular-challenges/shared/utils": ["libs/shared/utils/src/index.ts"], "@angular-challenges/testing-table/backend": [ "libs/testing-table/backend/src/index.ts"