feat(challenge37): ngfor optimize big list

This commit is contained in:
thomas
2023-10-07 20:58:25 +02:00
parent 6c2da1bcde
commit cdf6df005a
24 changed files with 418 additions and 8 deletions

View File

@@ -24,7 +24,7 @@ If you would like to propose a challenge, this project is open source, so feel f
## Challenges
Check [all 36 challenges](https://angular-challenges.vercel.app/)
Check [all 37 challenges](https://angular-challenges.vercel.app/)
## Contributors ✨

View File

@@ -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": {}
}
]
}

View File

@@ -0,0 +1,13 @@
# NgFor optimize big list
> Author: Thomas Laforge
### Run Application
```bash
npx nx serve performance-ngfor-biglist
```
### Documentation and Instruction
Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular-performance/37-ngfor-biglist/).

View File

@@ -0,0 +1,81 @@
{
"name": "performance-ngfor-biglist",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"prefix": "app",
"sourceRoot": "apps/performance/ngfor-biglist/src",
"tags": [],
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:browser",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/performance/ngfor-biglist",
"index": "apps/performance/ngfor-biglist/src/index.html",
"main": "apps/performance/ngfor-biglist/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "apps/performance/ngfor-biglist/tsconfig.app.json",
"assets": [
"apps/performance/ngfor-biglist/src/favicon.ico",
"apps/performance/ngfor-biglist/src/assets"
],
"styles": ["apps/performance/ngfor-biglist/src/styles.scss"],
"scripts": []
},
"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-biglist:build:production"
},
"development": {
"browserTarget": "performance-ngfor-biglist:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "performance-ngfor-biglist:build"
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
"apps/performance/ngfor-biglist/**/*.ts",
"apps/performance/ngfor-biglist/**/*.html"
]
}
}
}
}

View File

@@ -0,0 +1,42 @@
import { NgIf } from '@angular/common';
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { generateList } from './generateList';
import { PersonService } from './list.service';
import { PersonListComponent } from './person-list.component';
@Component({
standalone: true,
imports: [
NgIf,
PersonListComponent,
FormsModule,
MatFormFieldModule,
MatInputModule,
],
providers: [PersonService],
selector: 'app-root',
template: `
<button
(click)="loadList.set(true)"
class="border border-black p-2 rounded-md">
Load List
</button>
<app-person-list
*ngIf="loadList()"
class="max-w-2xl w-3/4"
[persons]="persons()" />
`,
host: {
class: 'flex items-center flex-col gap-5',
},
})
export class AppComponent {
readonly persons = signal(generateList());
readonly loadList = signal(false);
label = '';
}

View File

@@ -0,0 +1,6 @@
import { ApplicationConfig } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
export const appConfig: ApplicationConfig = {
providers: [provideAnimations()],
};

View File

@@ -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 < 100000; i++) {
arr.push({
email: randEmail(),
name: randFirstName(),
});
}
return arr;
}

View File

@@ -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<Person[]>([]);
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 })),
];
}

View File

@@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { NgForTrackByModule } from '@angular-challenges/shared/directives';
import { CommonModule } from '@angular/common';
import { Person } from './person.model';
@Component({
selector: 'app-person-list',
standalone: true,
imports: [CommonModule, NgForTrackByModule],
template: `
<div class="h-[300px] relative overflow-hidden">
<div class="absolute inset-0 overflow-scroll">
<div
*ngFor="let person of persons; trackByProp: 'email'"
class="flex justify-between items-center border-b h-9">
<h3>{{ person.name }}</h3>
<p>{{ person.email }}</p>
</div>
</div>
</div>
`,
host: {
class: 'w-full flex flex-col',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PersonListComponent {
@Input() persons: Person[] = [];
}

View File

@@ -0,0 +1,4 @@
export interface Person {
email: string;
name: string;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>performance-ngfor-biglist</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -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)
);

View File

@@ -0,0 +1,5 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* You can add global styles to this file, and also import other style files */

View File

@@ -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: [],
};

View File

@@ -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"]
}

View File

@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"include": ["src/**/*.ts"],
"compilerOptions": {
"types": []
}
}

View File

@@ -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.app.json"
},
{
"path": "./tsconfig.editor.json"
}
],
"extends": "../../../tsconfig.base.json",
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -1,6 +1,6 @@
{
"total": 36,
"total": 37,
"🟢": 13,
"🟠": 116,
"🟠": 117,
"🔴": 207
}

View File

@@ -1,6 +1,6 @@
---
title: 🟢 Memoization
description: Challenge 35 is about learning
description: Challenge 35 is about learning how pure pipe works
sidebar:
order: 8
---

View File

@@ -1,9 +1,8 @@
---
title: 🟢 NgFor Optimization
description: Challenge 36 is about ...
description: Challenge 36 is about learning how trackby works
sidebar:
order: 13
badge: New
---
<div class="chip">Challenge #36</div>

View File

@@ -0,0 +1,55 @@
---
title: 🟠 NgFor Optimization Big List
description: Challenge 37 is about learning how virtualization optimize big list rendering
sidebar:
order: 117
badge: New
---
<div class="chip">Challenge #37</div>
## Information
In this application, we can render a list of 100,000 individuals by clicking on the **loadList** button. If you open the Chrome developer panel by pressing **F12**, go to the <b>Source</b> tab, and expand the element to see the list, you will notice that all 100,000 elements are rendered in the DOM, even though we can only see about 20 elements in the viewport. This process takes a lot of time, which is why the application is very slow at displaying the list.
We can use the <b>Angular DevTool</b> 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.
:::
## Statement
The goal of this challenge is to implement a better alternative to display big list of items.
## Hints:
<details>
<summary>Hint 1</summary>
If you're unsure where to begin, I recommend reading the [Angular CDK virtualization documentation](https://material.angular.io/cdk/scrolling/overview)
</details>
---
:::note
Start the project by running: `npx nx serve performance-ngfor-biglist`.
:::
:::tip[Reminder]
Your PR title must start with <b>Answer:37</b>.
:::
<div class="article-footer">
<a
href="https://github.com/tomalaforge/angular-challenges/pulls?q=label%3A37+label%3Aanswer"
alt="NgFor optimize big list community solutions">
❖ Community Answers
</a>
<a
href='https://github.com/tomalaforge/angular-challenges/pulls?q=label%3A37+label%3A"answer+author"'
alt="NgFor optimize big list solution author">
▶︎ Author Answer
</a>
</div>

View File

@@ -23,8 +23,8 @@ hero:
import { Card, CardGrid } from '@astrojs/starlight/components';
<CardGrid>
<Card title="36 Challenges">
This repository gathers 36 Challenges related to <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> and <b>Typescript</b>.
<Card title="37 Challenges">
This repository gathers 37 Challenges related to <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> and <b>Typescript</b>.
These challenges resolve around real-life issues or specific features to elevate your skills.
</Card>