feat(challenge 59): contente projection defer

This commit is contained in:
thomas
2025-03-30 22:37:45 +02:00
parent 95899e2ea8
commit 99a3647cb5
29 changed files with 3908 additions and 1985 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 58 challenges](https://angular-challenges.vercel.app/)
Check [all 59 challenges](https://angular-challenges.vercel.app/)
## Contributors ✨

View File

@@ -0,0 +1,37 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts"],
"extends": [
"plugin:@nx/angular",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/component-class-suffix": "off",
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
]
}
},
{
"files": ["*.html"],
"extends": ["plugin:@nx/angular-template"],
"rules": {}
}
]
}

View File

@@ -0,0 +1,13 @@
# content-projection-defer
> author: thomas-laforge
### Run Application
```bash
npx nx serve angular-content-projection-defer
```
### Documentation and Instruction
Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/59-content-projection-defer/).

View File

@@ -0,0 +1,82 @@
{
"name": "angular-content-projection-defer",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"prefix": "app",
"sourceRoot": "apps/angular/59-content-projection-defer/src",
"tags": [],
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:application",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/angular/59-content-projection-defer",
"index": "apps/angular/59-content-projection-defer/src/index.html",
"browser": "apps/angular/59-content-projection-defer/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "apps/angular/59-content-projection-defer/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "apps/angular/59-content-projection-defer/public"
}
],
"styles": ["apps/angular/59-content-projection-defer/src/styles.scss"],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kb",
"maximumError": "8kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"executor": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "angular-content-projection-defer:build:production"
},
"development": {
"buildTarget": "angular-content-projection-defer:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "angular-content-projection-defer:build"
}
},
"lint": {
"executor": "@nx/eslint:lint"
},
"serve-static": {
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "angular-content-projection-defer:build",
"staticFilePath": "dist/apps/angular/59-content-projection-defer/browser",
"spa": true
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,23 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
@Component({
imports: [RouterOutlet, RouterLink],
selector: 'app-root',
template: `
<div class="flex gap-2">
<button class="rounded-md border px-4 py-2" routerLink="/page-1">
Page 1
</button>
<button class="rounded-md border px-4 py-2" routerLink="/page-2">
Page 2
</button>
</div>
<router-outlet />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
class: 'flex flex-col gap-2 ',
},
})
export class AppComponent {}

View File

@@ -0,0 +1,12 @@
import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { appRoutes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(appRoutes),
provideHttpClient(),
],
};

View File

@@ -0,0 +1,13 @@
import { Route } from '@angular/router';
export const appRoutes: Route[] = [
{
path: 'page-1',
loadComponent: () => import('./page-1').then((m) => m.Page1),
},
{
path: 'page-2',
loadComponent: () => import('./page-2').then((m) => m.Page2),
},
{ path: '**', redirectTo: 'page-1' },
];

View File

@@ -0,0 +1,54 @@
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
@Component({
selector: 'app-expandable-card',
template: `
<button
class="text-fg-subtle hover:bg-button-secondary-bg-hover active:bg-button-secondary-bg-active focus:outline-button-border-highlight flex w-fit items-center gap-1 py-2 focus:outline focus:outline-2 focus:outline-offset-1"
(click)="isExpanded.set(!isExpanded())"
data-cy="expandable-panel-toggle">
@if (isExpanded()) {
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-4 w-4">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
} @else {
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-4 w-4">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
}
<ng-content select="[title]" />
</button>
<div
class="overflow-hidden transition-[max-height] duration-500"
[class.max-h-0]="!isExpanded()"
[class.max-h-[1000px]]="isExpanded()">
<ng-content />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
class: 'flex flex-col gap-2 ',
},
})
export class ExpandableCard {
public isExpanded = signal(false);
}

View File

@@ -0,0 +1,10 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-page-1',
template: `
page1
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Page1 {}

View File

@@ -0,0 +1,43 @@
import { httpResource } from '@angular/common/http';
import {
ChangeDetectionStrategy,
Component,
ResourceStatus,
} from '@angular/core';
import { ExpandableCard } from './expandable-card';
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
@Component({
selector: 'app-page-2',
template: `
page2
<app-expandable-card>
<div title>Load Post</div>
<div>
@if (postRessource.isLoading()) {
Loading...
} @else if (postRessource.status() === ResourceStatus.Error) {
Error...
} @else {
@for (post of postRessource.value(); track post.id) {
<div>{{ post.title }}</div>
}
}
</div>
</app-expandable-card>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ExpandableCard],
})
export class Page2 {
public postRessource = httpResource<Post[]>(
'https://jsonplaceholder.typicode.com/posts',
);
protected readonly ResourceStatus = ResourceStatus;
}

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>angular-content-projection-defer</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 { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
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,6 @@
{
"extends": "./tsconfig.json",
"include": ["src/**/*.ts"],
"compilerOptions": {},
"exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
}

View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "es2022",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.editor.json"
},
{
"path": "./tsconfig.app.json"
}
],
"extends": "../../../tsconfig.base.json",
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -1,6 +1,6 @@
{
"total": 58,
"total": 59,
"🟢": 22,
"🟠": 124,
"🔴": 211
"🔴": 212
}

View File

@@ -8,7 +8,6 @@ challengeNumber: 58
command: angular-content-projection-condition
sidebar:
order: 124
badge: New
---
## Information

View File

@@ -0,0 +1,28 @@
---
title: 🔴 content-projection-defer
description: Challenge 59 is about deferring fetching data
author: thomas-laforge
contributors:
- tomalaforge
challengeNumber: 59
command: angular-content-projection-defer
sidebar:
order: 212
badge: New
---
# Challenge: Deferred Loading for Expandable Card Content
## Information
Within the application, specifically on page2, there is an expandable card component. This component consists of a permanently visible title and a content section that is hidden until the card is expanded. This content section is populated with a list of posts retrieved via a backend API call. The current implementation presents an issue: upon navigating to page2, although the card defaults to a collapsed state, the API call to load the list of posts is triggered immediately during the page load process, before the user has chosen to expand the card and view the content.
## Statement
The goal of this challenge is to optimize the data loading behavior for the expandable card component on `page2`. Modify the implementation so that the backend API call to fetch the list of posts is **deferred**. The data should **only** be fetched when the user explicitly interacts with the card to **expand** it. No data fetching for the post list should occur upon the initial load of `page2` while the card remains collapsed.
## Constraints
- The expandable card must retain its core functionality: display a title, be initially collapsed (on `page2` load), and expand/collapse upon user interaction.
- When the card is expanded, the list of posts must be fetched from the backend and displayed within the content area.
- The data fetching mechanism itself (e.g., the API endpoint) should not be changed, only _when_ it is triggered.

View File

@@ -13,7 +13,7 @@ hero:
icon: right-arrow
variant: primary
- text: Ir al Desafío más reciente
link: /es/challenges/angular/58-content-projection-condition/
link: /es/challenges/angular/59-content-projection-defer/
icon: rocket
- text: Dar una estrella
link: https://github.com/tomalaforge/angular-challenges
@@ -26,8 +26,8 @@ import MyIcon from '../../../components/MyIcon.astro';
import SubscriptionForm from '../../../components/SubscriptionForm.astro';
<CardGrid>
<Card title="58 Desafíos">
Este repositorio contiene 58 Desafíos relacionados con <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> y <b>Typescript</b>.
<Card title="59 Desafíos">
Este repositorio contiene 59 Desafíos relacionados con <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> y <b>Typescript</b>.
Estos desafíos se resuelven en torno a problemas de la vida real o características específicas para mejorar tus habilidades.
</Card>

View File

@@ -13,7 +13,7 @@ hero:
icon: right-arrow
variant: primary
- text: Aller au dernier Challenge
link: /fr/challenges/angular/58-content-projection-condition/
link: /fr/challenges/angular/59-content-projection-defer/
icon: rocket
- text: Donne une étoile
link: https://github.com/tomalaforge/angular-challenges
@@ -26,8 +26,8 @@ import MyIcon from '../../../components/MyIcon.astro';
import SubscriptionForm from '../../../components/SubscriptionForm.astro';
<CardGrid>
<Card title="58 Défis">
Ce répertoire rassemble 58 Défis liés à <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> et <b>Typescript</b>. Ces défis portent sur des problèmes réels ou des fonctionnalités spécifiques pour améliorer vos compétences.
<Card title="59 Défis">
Ce répertoire rassemble 59 Défis liés à <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> et <b>Typescript</b>. Ces défis portent sur des problèmes réels ou des fonctionnalités spécifiques pour améliorer vos compétences.
</Card>
<Card title="Subscribe to get notify of latest challenges">

View File

@@ -13,7 +13,7 @@ hero:
icon: right-arrow
variant: primary
- text: Go to the latest Challenge
link: /challenges/angular/58-content-projection-condition/
link: /challenges/angular/59-content-projection-defer/
icon: rocket
- text: Give a star
link: https://github.com/tomalaforge/angular-challenges
@@ -27,8 +27,8 @@ import MyIcon from '../../components/MyIcon.astro';
import SubscriptionForm from '../../components/SubscriptionForm.astro';
<CardGrid>
<Card title="58 Challenges">
This repository gathers 58 Challenges related to <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> and <b>Typescript</b>.
<Card title="59 Challenges">
This repository gathers 59 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>

View File

@@ -13,7 +13,7 @@ hero:
icon: right-arrow
variant: primary
- text: Ir para o desafio mais recente
link: /pt/challenges/angular/58-content-projection-condition/
link: /pt/challenges/angular/59-content-projection-defer/
icon: rocket
- text: Dar uma estrela
link: https://github.com/tomalaforge/angular-challenges
@@ -26,8 +26,8 @@ import MyIcon from '../../../components/MyIcon.astro';
import SubscriptionForm from '../../../components/SubscriptionForm.astro';
<CardGrid>
<Card title="58 Desafios">
Este repositório possui 58 Desafios relacionados a <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>,
<Card title="59 Desafios">
Este repositório possui 59 Desafios relacionados a <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>,
<b>Ngrx</b> e <b>Typescript</b>.
Esses desafios são voltados para problemas reais ou funcionalidades específicas afim de
melhorar suas habilidades.

View File

@@ -13,7 +13,7 @@ hero:
icon: right-arrow
variant: primary
- text: Перейти к последней задаче
link: /ru/challenges/angular/58-content-projection-condition/
link: /ru/challenges/angular/59-content-projection-defer/
icon: rocket
- text: Добавить звезду
link: https://github.com/tomalaforge/angular-challenges

View File

@@ -1,28 +1,13 @@
{
"migrations": [
{
"version": "20.3.0-beta.1",
"description": "Update ESLint flat config to include .cjs, .mjs, .cts, and .mts files in overrides (if needed)",
"implementation": "./src/migrations/update-20-3-0/add-file-extensions-to-overrides",
"package": "@nx/eslint",
"name": "add-file-extensions-to-overrides"
},
{
"cli": "nx",
"version": "20.3.0-beta.2",
"description": "If workspace includes Module Federation projects, ensure the new @nx/module-federation package is installed.",
"factory": "./src/migrations/update-20-3-0/ensure-nx-module-federation-package",
"version": "20.5.0-beta.5",
"requires": { "@angular/core": ">=19.2.0" },
"description": "Update the @angular/cli package version to ~19.2.0.",
"factory": "./src/migrations/update-20-5-0/update-angular-cli",
"package": "@nx/angular",
"name": "ensure-nx-module-federation-package"
},
{
"cli": "nx",
"version": "20.4.0-beta.1",
"requires": { "@angular/core": ">=19.1.0" },
"description": "Update the @angular/cli package version to ~19.1.0.",
"factory": "./src/migrations/update-20-4-0/update-angular-cli",
"package": "@nx/angular",
"name": "update-angular-cli-version-19-1-0"
"name": "update-angular-cli-version-19-2-0"
}
]
}

5372
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,16 +14,16 @@
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0",
"@angular/animations": "19.1.5",
"@angular/cdk": "19.1.3",
"@angular/common": "19.1.5",
"@angular/compiler": "19.1.5",
"@angular/core": "19.1.5",
"@angular/forms": "19.1.5",
"@angular/material": "19.1.3",
"@angular/platform-browser": "19.1.5",
"@angular/platform-browser-dynamic": "19.1.5",
"@angular/router": "19.1.5",
"@angular/animations": "19.2.4",
"@angular/cdk": "19.2.7",
"@angular/common": "19.2.4",
"@angular/compiler": "19.2.4",
"@angular/core": "19.2.4",
"@angular/forms": "19.2.4",
"@angular/material": "19.2.7",
"@angular/platform-browser": "19.2.4",
"@angular/platform-browser-dynamic": "19.2.4",
"@angular/router": "19.2.4",
"@ngneat/falso": "7.2.0",
"@ngrx/component": "19.0.1",
"@ngrx/component-store": "19.0.1",
@@ -32,7 +32,7 @@
"@ngrx/operators": "19.0.1",
"@ngrx/router-store": "19.0.1",
"@ngrx/store": "19.0.1",
"@nx/angular": "20.4.1",
"@nx/angular": "20.6.4",
"@swc/helpers": "0.5.12",
"@tanstack/angular-query-experimental": "5.62.3",
"rxjs": "7.8.1",
@@ -41,30 +41,30 @@
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "19.1.6",
"@angular-devkit/core": "19.1.6",
"@angular-devkit/schematics": "19.1.6",
"@angular-eslint/eslint-plugin": "19.0.2",
"@angular-eslint/eslint-plugin-template": "19.0.2",
"@angular-eslint/template-parser": "19.0.2",
"@angular/cli": "19.1.6",
"@angular/compiler-cli": "19.1.5",
"@angular/language-service": "19.1.5",
"@angular-devkit/build-angular": "19.2.5",
"@angular-devkit/core": "19.2.5",
"@angular-devkit/schematics": "19.2.5",
"@angular-eslint/eslint-plugin": "19.3.0",
"@angular-eslint/eslint-plugin-template": "19.3.0",
"@angular-eslint/template-parser": "19.3.0",
"@angular/cli": "~19.2.0",
"@angular/compiler-cli": "19.2.4",
"@angular/language-service": "19.2.4",
"@commitlint/cli": "^17.2.0",
"@commitlint/config-conventional": "^17.2.0",
"@cypress/webpack-dev-server": "3.8.0",
"@hirez_io/observer-spy": "^2.2.0",
"@ngrx/schematics": "19.0.1",
"@nx/cypress": "20.4.1",
"@nx/devkit": "20.4.1",
"@nx/eslint": "20.4.1",
"@nx/eslint-plugin": "20.4.1",
"@nx/jest": "20.4.1",
"@nx/js": "20.4.1",
"@nx/plugin": "20.4.1",
"@nx/web": "20.4.1",
"@nx/workspace": "20.4.1",
"@schematics/angular": "19.1.6",
"@nx/cypress": "20.6.4",
"@nx/devkit": "20.6.4",
"@nx/eslint": "20.6.4",
"@nx/eslint-plugin": "20.6.4",
"@nx/jest": "20.6.4",
"@nx/js": "20.6.4",
"@nx/plugin": "20.6.4",
"@nx/web": "20.6.4",
"@nx/workspace": "20.6.4",
"@schematics/angular": "19.2.5",
"@swc-node/register": "1.9.2",
"@swc/cli": "0.5.2",
"@swc/core": "1.10.0",
@@ -92,8 +92,8 @@
"jest-preset-angular": "14.4.2",
"jsonc-eslint-parser": "^2.1.0",
"lint-staged": "^13.0.3",
"ng-packagr": "19.1.2",
"nx": "20.4.1",
"ng-packagr": "19.2.0",
"nx": "20.6.4",
"postcss": "^8.4.5",
"postcss-import": "~14.1.0",
"postcss-preset-env": "~7.5.0",