diff --git a/.all-contributorsrc b/.all-contributorsrc
new file mode 100644
index 000000000..a84f812d4
--- /dev/null
+++ b/.all-contributorsrc
@@ -0,0 +1,389 @@
+{
+ "projectName": "angular-challenges",
+ "projectOwner": "tomalaforge",
+ "repoType": "github",
+ "repoHost": "https://github.com",
+ "files": [
+ "README.md"
+ ],
+ "imageSize": 100,
+ "commit": true,
+ "commitConvention": "angular",
+ "types": {
+ "challenge": {
+ "symbol": "🧩",
+ "description": "Create a challenge"
+ },
+ "translation-es": {
+ "symbol": "🇪🇸",
+ "description": "Translate in Spanish"
+ },
+ "translation-fr": {
+ "symbol": "🇫🇷",
+ "description": "Translate in French"
+ },
+ "translation-pt": {
+ "symbol": "🇵🇹",
+ "description": "Translate in Portuguese"
+ },
+ "translation-ru": {
+ "symbol": "🇷🇺",
+ "description": "Translate in Russian"
+ },
+ "translation-ch": {
+ "symbol": "🇨🇳",
+ "description": "Translate in Chinese"
+ }
+ },
+ "contributors": [
+ {
+ "login": "tomalaforge",
+ "name": "Laforge Thomas",
+ "avatar_url": "https://avatars.githubusercontent.com/u/30832608?v=4",
+ "profile": "https://thomaslaforge.dev/home",
+ "contributions": [
+ "challenge",
+ "code",
+ "doc",
+ "content",
+ "ideas",
+ "design",
+ "translation-fr"
+ ]
+ },
+ {
+ "login": "svenson95",
+ "name": "Sven Brodny",
+ "avatar_url": "https://avatars.githubusercontent.com/u/46655156?v=4",
+ "profile": "https://svenson95.github.io/sb-portfolio/",
+ "contributions": [
+ "doc",
+ "challenge",
+ "content",
+ "design"
+ ]
+ },
+ {
+ "login": "jdegand",
+ "name": "J. Degand",
+ "avatar_url": "https://avatars.githubusercontent.com/u/70610011?v=4",
+ "profile": "https://github.com/jdegand",
+ "contributions": [
+ "doc",
+ "content",
+ "code"
+ ]
+ },
+ {
+ "login": "DeveshChau",
+ "name": "Devesh Chaudhari",
+ "avatar_url": "https://avatars.githubusercontent.com/u/9509673?v=4",
+ "profile": "https://github.com/DeveshChau",
+ "contributions": [
+ "code",
+ "bug",
+ "challenge"
+ ]
+ },
+ {
+ "login": "stillst",
+ "name": "stillst",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1463098?v=4",
+ "profile": "https://github.com/stillst",
+ "contributions": [
+ "challenge",
+ "translation-ru"
+ ]
+ },
+ {
+ "login": "wandri",
+ "name": "Wandrille",
+ "avatar_url": "https://avatars.githubusercontent.com/u/15016833?v=4",
+ "profile": "https://wandrille-guesdon.com/",
+ "contributions": [
+ "challenge"
+ ]
+ },
+ {
+ "login": "alcaidio",
+ "name": "Timothy Alcaide",
+ "avatar_url": "https://avatars.githubusercontent.com/u/17033036?v=4",
+ "profile": "https://twitter.com/alcaidio",
+ "contributions": [
+ "challenge"
+ ]
+ },
+ {
+ "login": "LMFinney",
+ "name": "Lance Finney",
+ "avatar_url": "https://avatars.githubusercontent.com/u/6683747?v=4",
+ "profile": "https://github.com/LMFinney",
+ "contributions": [
+ "doc",
+ "challenge"
+ ]
+ },
+ {
+ "login": "tsironis13",
+ "name": "Tsironis Ioannis",
+ "avatar_url": "https://avatars.githubusercontent.com/u/7561447?v=4",
+ "profile": "https://github.com/tsironis13",
+ "contributions": [
+ "challenge"
+ ]
+ },
+ {
+ "login": "alan-bio",
+ "name": "Alan Dragicevich",
+ "avatar_url": "https://avatars.githubusercontent.com/u/31838230?v=4",
+ "profile": "https://github.com/alan-bio",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "edimitchel",
+ "name": "Michel EDIGHOFFER",
+ "avatar_url": "https://avatars.githubusercontent.com/u/2922851?v=4",
+ "profile": "https://github.com/edimitchel",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "gsgonzalez88",
+ "name": "Gerardo Sebastian Gonzalez",
+ "avatar_url": "https://avatars.githubusercontent.com/u/39884678?v=4",
+ "profile": "https://github.com/gsgonzalez88",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "marryday",
+ "name": "Evseev Yuriy",
+ "avatar_url": "https://avatars.githubusercontent.com/u/57489315?v=4",
+ "profile": "https://github.com/marryday",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "tomer953",
+ "name": "Tomer953",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1807493?v=4",
+ "profile": "https://github.com/tomer953",
+ "contributions": [
+ "bug",
+ "doc",
+ "code"
+ ]
+ },
+ {
+ "login": "dmmishchenko",
+ "name": "Dmitriy Mishchenko",
+ "avatar_url": "https://avatars.githubusercontent.com/u/51910160?v=4",
+ "profile": "https://github.com/dmmishchenko",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "Sagardevkota",
+ "name": "Sagar Devkota",
+ "avatar_url": "https://avatars.githubusercontent.com/u/30800393?v=4",
+ "profile": "http://www.sagardev.com.np",
+ "contributions": [
+ "doc",
+ "code"
+ ]
+ },
+ {
+ "login": "nelsongutidev",
+ "name": "Nelson Gutierrez",
+ "avatar_url": "https://avatars.githubusercontent.com/u/62297014?v=4",
+ "profile": "https://nelsonguti.dev/",
+ "contributions": [
+ "translation-es"
+ ]
+ },
+ {
+ "login": "ho-ssain",
+ "name": "Hossain K. M.",
+ "avatar_url": "https://avatars.githubusercontent.com/u/61125174?v=4",
+ "profile": "https://github.com/ho-ssain",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "kabrunko-dev",
+ "name": "Diogo Nishikawa",
+ "avatar_url": "https://avatars.githubusercontent.com/u/142346548?v=4",
+ "profile": "https://github.com/kabrunko-dev/",
+ "contributions": [
+ "code",
+ "translation-pt",
+ "doc"
+ ]
+ },
+ {
+ "login": "ErickRodrCodes",
+ "name": "Erick Rodriguez",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1978642?v=4",
+ "profile": "http://www.streamoverlaypro.com",
+ "contributions": [
+ "translation-es"
+ ]
+ },
+ {
+ "login": "eduardoRoth",
+ "name": "Eduardo Roth",
+ "avatar_url": "https://avatars.githubusercontent.com/u/5419161?v=4",
+ "profile": "https://eduardoroth.dev",
+ "contributions": [
+ "doc",
+ "translation-es"
+ ]
+ },
+ {
+ "login": "1fbr",
+ "name": "Fernando Bello",
+ "avatar_url": "https://avatars.githubusercontent.com/u/63980689?v=4",
+ "profile": "https://github.com/1fbr",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "webbomj",
+ "name": "Лапин Андрей (Lapin Andrey)",
+ "avatar_url": "https://avatars.githubusercontent.com/u/86595717?v=4",
+ "profile": "https://github.com/webbomj",
+ "contributions": [
+ "translation-ru"
+ ]
+ },
+ {
+ "login": "Dinozavvvr",
+ "name": "Dinar Shagaliev",
+ "avatar_url": "https://avatars.githubusercontent.com/u/45518871?v=4",
+ "profile": "https://github.com/Dinozavvvr",
+ "contributions": [
+ "translation-ru"
+ ]
+ },
+ {
+ "login": "vimulatus",
+ "name": "Vimulatus",
+ "avatar_url": "https://avatars.githubusercontent.com/u/63696128?v=4",
+ "profile": "https://github.com/vimulatus",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "alannelucq",
+ "name": "Arthur LANNELUCQ",
+ "avatar_url": "https://avatars.githubusercontent.com/u/44091408?v=4",
+ "profile": "https://github.com/alannelucq",
+ "contributions": [
+ "translation-fr"
+ ]
+ },
+ {
+ "login": "fixedmichal",
+ "name": "fixed_michal",
+ "avatar_url": "https://avatars.githubusercontent.com/u/26270192?v=4",
+ "profile": "https://github.com/fixedmichal",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "Tenessy",
+ "name": "Tenessy",
+ "avatar_url": "https://avatars.githubusercontent.com/u/65855673?v=4",
+ "profile": "https://github.com/Tenessy",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "EnochGao",
+ "name": "Enoch Gao",
+ "avatar_url": "https://avatars.githubusercontent.com/u/41459067?v=4",
+ "profile": "https://enochgao.github.io/",
+ "contributions": [
+ "doc",
+ "translation-ch"
+ ]
+ },
+ {
+ "login": "fpalmab",
+ "name": "Francisco Palma",
+ "avatar_url": "https://avatars.githubusercontent.com/u/7729812?v=4",
+ "profile": "https://github.com/fpalmab",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "tamim36",
+ "name": "Tamim Arefin Anik",
+ "avatar_url": "https://avatars.githubusercontent.com/u/42251521?v=4",
+ "profile": "https://github.com/tamim36",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "WhoisBsa",
+ "name": "Matheus B.",
+ "avatar_url": "https://avatars.githubusercontent.com/u/36895235?v=4",
+ "profile": "https://github.com/WhoisBsa",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "StefH",
+ "name": "Stef Heyenrath",
+ "avatar_url": "https://avatars.githubusercontent.com/u/249938?v=4",
+ "profile": "https://sourcerer.io/stefh",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "mathisvester",
+ "name": "mathisvester",
+ "avatar_url": "https://avatars.githubusercontent.com/u/26164587?v=4",
+ "profile": "https://github.com/mathisvester",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "Banderstadt",
+ "name": "Vasyl Zadorozhnyi",
+ "avatar_url": "https://avatars.githubusercontent.com/u/24597998?v=4",
+ "profile": "https://github.com/Banderstadt",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "bertandfrogs",
+ "name": "Sarah Smalley",
+ "avatar_url": "https://avatars.githubusercontent.com/u/25539891?v=4",
+ "profile": "https://github.com/bertandfrogs",
+ "contributions": [
+ "bug"
+ ]
+ }
+ ],
+ "contributorsPerLine": 7,
+ "linkToUsage": true,
+ "commitType": "docs"
+}
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 000000000..3c3629e64
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+node_modules
diff --git a/.eslintrc.json b/.eslintrc.json
index aaddd5e07..de9b234b6 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,12 +1,13 @@
{
"root": true,
"ignorePatterns": ["**/*"],
- "plugins": ["@nrwl/nx"],
+ "plugins": ["@nx"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
- "@nrwl/nx/enforce-module-boundaries": [
+ "@angular-eslint/no-host-metadata-property": "off",
+ "@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
@@ -18,19 +19,24 @@
}
]
}
- ],
- "@angular-eslint/no-host-metadata-property": ["error", { "allowStatic": true }]
+ ]
}
},
{
"files": ["*.ts", "*.tsx"],
- "extends": ["plugin:@nrwl/nx/typescript"],
- "rules": {}
+ "extends": ["plugin:@nx/typescript"],
+ "rules": {
+ "@typescript-eslint/no-extra-semi": "error",
+ "no-extra-semi": "off"
+ }
},
{
"files": ["*.js", "*.jsx"],
- "extends": ["plugin:@nrwl/nx/javascript"],
- "rules": {}
+ "extends": ["plugin:@nx/javascript"],
+ "rules": {
+ "@typescript-eslint/no-extra-semi": "error",
+ "no-extra-semi": "off"
+ }
},
{
"files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"],
@@ -38,6 +44,11 @@
"jest": true
},
"rules": {}
+ },
+ {
+ "files": "*.json",
+ "parser": "jsonc-eslint-parser",
+ "rules": {}
}
]
}
diff --git a/.github/copilot-guidelines.md b/.github/copilot-guidelines.md
new file mode 100644
index 000000000..e1b11d43a
--- /dev/null
+++ b/.github/copilot-guidelines.md
@@ -0,0 +1,46 @@
+# GitHub Copilot Custom Guidelines
+
+You are an expert in TypeScript, Angular, and scalable web application development. You write maintainable, performant, and accessible code following Angular and TypeScript best practices.
+
+## TypeScript Best Practices
+
+- Use strict type checking
+- Prefer type inference when the type is obvious
+- Avoid the `any` type; use `unknown` when type is uncertain
+
+## Angular Best Practices
+
+- Always use standalone components over NgModules
+- Don't use explicit `standalone: true` (it is implied by default)
+- Use signals for state management
+- Implement lazy loading for feature routes
+- Use `NgOptimizedImage` for all static images.
+
+## Components
+
+- Keep components small and focused on a single responsibility
+- Use `input()` and `output()` functions instead of decorators
+- Use `computed()` for derived state
+- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator
+- Prefer inline templates for small components
+- Prefer Reactive forms instead of Template-driven ones
+- Do NOT use `ngClass`, use `class` bindings instead
+- DO NOT use `ngStyle`, use `style` bindings instead
+
+## State Management
+
+- Use signals for local component state
+- Use `computed()` for derived state
+- Keep state transformations pure and predictable
+
+## Templates
+
+- Keep templates simple and avoid complex logic
+- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch`
+- Use the async pipe to handle observables
+
+## Services
+
+- Design services around a single responsibility
+- Use the `providedIn: 'root'` option for singleton services
+- Use the `inject()` function instead of constructor injection
diff --git a/.github/funding.yml b/.github/funding.yml
new file mode 100644
index 000000000..f1327048f
--- /dev/null
+++ b/.github/funding.yml
@@ -0,0 +1 @@
+github: [tomalaforge]
diff --git a/.github/github-action/action.yml b/.github/github-action/action.yml
new file mode 100644
index 000000000..8019592af
--- /dev/null
+++ b/.github/github-action/action.yml
@@ -0,0 +1,14 @@
+name: 'Hello World'
+description: 'Greet someone and record the time'
+inputs:
+ github_token:
+ description: A GitHub token.
+ required: false
+ default: ${{ github.token }}
+ repo:
+ description: The owner and repository name.
+ required: false
+ default: ${{ github.repository }}
+runs:
+ using: 'node20'
+ main: 'index.js'
diff --git a/.github/github-action/contributors.js b/.github/github-action/contributors.js
new file mode 100644
index 000000000..a8f1bcaa6
--- /dev/null
+++ b/.github/github-action/contributors.js
@@ -0,0 +1,37 @@
+const contributors = [
+ 'alcaidio',
+ 'svenson95',
+ 'jdegand',
+ 'DeveshChau',
+ 'stillst',
+ 'wandri',
+ 'webbomj',
+ 'kabrunko-dev',
+ 'Sanjar1304',
+ 'tsironis13',
+ 'EnochGao',
+];
+
+const sponsors = [
+ 'ddotx',
+ 'LMFinney',
+ 'alannelucq',
+ 'SidV2',
+ 'fpalmab',
+ 'CivilEngeneer',
+ 'apalaio',
+ 'amosISA',
+ 'michalgrzegorczyk-dev',
+ 'zealotrahl',
+ 'DzoeL123',
+ 'allan1989',
+ 'pchessah',
+ 'lukasss88',
+ 'danncortes',
+ 'Gw0zdziu'
+];
+
+module.exports = {
+ contributors,
+ sponsors,
+};
diff --git a/.github/github-action/index.js b/.github/github-action/index.js
new file mode 100644
index 000000000..a73e6c293
--- /dev/null
+++ b/.github/github-action/index.js
@@ -0,0 +1,45 @@
+const github = require('@actions/github');
+const core = require('@actions/core');
+const { contributors, sponsors } = require('./contributors');
+
+async function run() {
+ try {
+ const title = github.context.payload.pull_request.title;
+ const labels = ['answer'];
+
+ const match = title.match(/Answer(:?)\s*(\d+)/);
+ if (match) {
+ labels.push(String(parseInt(match[2], 10)));
+ }
+
+ const actor = github.context.actor;
+ if (contributors.includes(actor)) {
+ labels.push('contributor');
+ labels.push('to be reviewed');
+ }
+
+ if (sponsors.includes(actor)) {
+ labels.push('sponsor');
+ labels.push('to be reviewed');
+ }
+
+ const githubToken = core.getInput('github_token');
+
+ const number = github.context.payload.pull_request.number;
+
+ const octokit = github.getOctokit(githubToken);
+ await octokit.rest.issues.addLabels({
+ labels,
+ owner: github.context.repo.owner,
+ repo: github.context.repo.repo,
+ issue_number: number,
+ });
+ } catch (e) {
+ if (e instanceof Error) {
+ core.error(e);
+ core.setFailed(e.message);
+ }
+ }
+}
+
+run();
diff --git a/.github/workflows/close-inactive-pr.yml b/.github/workflows/close-inactive-pr.yml
new file mode 100644
index 000000000..d5e0e9c95
--- /dev/null
+++ b/.github/workflows/close-inactive-pr.yml
@@ -0,0 +1,28 @@
+name: Close inactive issues
+on:
+ schedule:
+ - cron: '0 0 * * *'
+
+jobs:
+ close-issues:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
+ steps:
+ - uses: actions/stale@v9
+ with:
+ days-before-issue-stale: 20
+ days-before-issue-close: -1
+ stale-issue-label: 'stale'
+ stale-issue-message: 'This issue is stale because it has been open for 15 days with no activity.'
+ exempt-issue-labels: 'long-term'
+ days-before-pr-stale: 20
+ days-before-pr-close: 7
+ stale-pr-label: 'stale'
+ stale-pr-message: 'This pull request is stale because it has been open for 15 days with no activity.'
+ close-pr-message: 'This pull request was closed because it has been inactive for 5 days since being marked as stale.'
+ only-pr-labels: 'answer'
+ exempt-pr-labels: 'challenge-creation, long-term, to be reviewed'
+ remove-pr-stale-when-updated: true
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/label-issue-update.yml b/.github/workflows/label-issue-update.yml
new file mode 100644
index 000000000..5f79b0568
--- /dev/null
+++ b/.github/workflows/label-issue-update.yml
@@ -0,0 +1,21 @@
+name: updates Labels
+
+on:
+ push:
+ branches-ignore:
+ - main
+
+jobs:
+ update_labels:
+ runs-on: ubuntu-latest
+ if: |
+ contains(github.event.pull_request.labels.*.name, 'sponsor') ||
+ contains(github.event.pull_request.labels.*.name, 'contributor')
+ steps:
+ - name: checkout
+ uses: actions/checkout@v2
+
+ - name: Add labels
+ uses: actions-ecosystem/action-add-labels@v1
+ with:
+ labels: to be reviewed/update
diff --git a/.github/workflows/label-issue.yml b/.github/workflows/label-issue.yml
new file mode 100644
index 000000000..555e42db5
--- /dev/null
+++ b/.github/workflows/label-issue.yml
@@ -0,0 +1,42 @@
+name: Add Labels
+
+on:
+ pull_request_target:
+ types: [ opened, edited, synchronize ]
+
+jobs:
+ check-title:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check PR title
+ env:
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ run: |
+ echo "Checking PR Title: '$PR_TITLE'"
+ if [[ ! "$PR_TITLE" =~ ^Answer: ]]; then
+ echo "❌ PR title should start with 'Answer:[#challenge number]'"
+ echo "### ❌ PR title should start with 'Answer:[#challenge number]'" >> $GITHUB_STEP_SUMMARY
+ exit 1
+ else
+ echo "✅ PR title format is correct."
+ echo "### ✅ PR title format is correct." >> $GITHUB_STEP_SUMMARY
+ fi
+ add_labels:
+ runs-on: ubuntu-latest
+ if: ${{ startsWith(github.event.pull_request.title, 'Answer') }}
+ steps:
+ - name: checkout
+ uses: actions/checkout@v2
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10
+
+ - name: Install dependencies
+ run: pnpm add @actions/core @actions/github
+
+ - name: Add labels
+ uses: ./.github/github-action/
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 204e1b2e1..8fb71de22 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,7 +19,6 @@ node_modules
# IDE - VSCode
.vscode/*
-!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
@@ -39,3 +38,16 @@ testem.log
Thumbs.db
.angular
+
+TODO.md
+.nx/cache
+.nx/workspace-data
+
+.cursorrules
+.cursor/rules/nx-rules.mdc
+.github/instructions/nx.instructions.md
+
+package-lock.json
+npm-shrinkwrap.json
+
+__screenshots__/
diff --git a/.prettierignore b/.prettierignore
index d0b804da2..a4684ea74 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,3 +2,8 @@
/dist
/coverage
+
+.angular
+
+/.nx/cache
+/.nx/workspace-data
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
index dea17d419..810823b36 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,4 +1,6 @@
{
"singleQuote": true,
- "bracketSameLine": true
+ "bracketSameLine": true,
+ "htmlWhitespaceSensitivity": "ignore",
+ "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"]
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..e0a129166
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,6 @@
+{
+ "eslint.validate": [
+ "json"
+ ],
+ "cSpell.language": "en,es-ES"
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e8fe2e7ea..44469ae9b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,24 +1,3 @@
# Contributing
-> Thank you for considering contributing to this project. Your help is very much appreciated!
-
-When contributing, it's better to first explain the challenge/exercice you are thinking about in the issue tab.
-
-## Getting started
-
-Please follow those step in order to succesfully make your contribution to this repository.
-
-1. Fork the project
-2. Install **Nx Console**, this will help you work with this repository
-3. Run `npm ci` to install all dependencies
-4. Generate a new app with Nx Console > Right Click on apps folder > `Nx Generate Application`
-5. Copy/Paste **example.README.md** and fill it up.
-6. Link the main **README** with your new challenge
-7. Few days later, create a PR with your answer.
-8. Optional: write a blog post explaining your Challenge and the solution you came up with.
-
-## Pull Request Process
-
-1. We follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/)
- in our commit messages, i.e. `feat(core): improve typing`
-2. When you are ready, create Pull Request of your fork into original repository with the title starting with **NEW CHALLENGE**
+Learn how to contribute [here](https://angular-challenges.vercel.app/guides/contribute/)
diff --git a/README.md b/README.md
index 27dbf6bf8..5bea1a4fa 100644
--- a/README.md
+++ b/README.md
@@ -2,51 +2,110 @@
-Collection of Angular challenges
+Collection of Angular challenges
-## Intro
-
-This goal of this project is to help you get better at Angular and NgRx by resolving challenges you might face in your day life.
+> [!WARNING]
+> This project now uses **pnpm** as its package manager. Please make sure you have pnpm installed before running any commands. You can install it globally with `npm install -g pnpm` or follow the [official installation guide](https://pnpm.io/installation).
-> **Learning by reading is good but learning by doing is better.**
-
-## Challenges
+
-> Click the following badges to join your next challenge.
->
->
->
->
+## Intro
-
-
+This project has been created with three purposes:
-
-
-
-
-
-
-
-
+- The first purpose is to assist you in becoming better at Angular and its ecosystem. 💪
+- The second purpose is to share best practices and different implementations of the same problem to gain diverse perspectives. 📖
+- The last one is to lower the barrier to open source contribution.
-
+**Sharing knowledge can benefit everyone.**
-
-
+Furthermore, if you are struggling with a concept, please open an issue. I will attempt to create a challenge based on that idea.
-
+If you would like to propose a challenge, this project is open source, so feel free to share your ideas. They are more than welcome.
-
+> **Learning by reading is good but learning by doing is better.**
-
-
+## Challenges
-
-
+Check [all 61 challenges](https://angular-challenges.vercel.app/)
+
+## Contributors ✨
+
+
+
+
+
+
+
+
+
+
+
+Contributions of any kind are welcome.
+
+If I have forgotten to add you as a contributor, please reach out to me. 🙏
## License
diff --git a/apps/angular/1-projection/.eslintrc.json b/apps/angular/1-projection/.eslintrc.json
new file mode 100644
index 000000000..d3cd7997a
--- /dev/null
+++ b/apps/angular/1-projection/.eslintrc.json
@@ -0,0 +1,19 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {}
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/apps/angular/1-projection/README.md b/apps/angular/1-projection/README.md
new file mode 100644
index 000000000..781198ead
--- /dev/null
+++ b/apps/angular/1-projection/README.md
@@ -0,0 +1,13 @@
+# Projection
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-projection
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/1-projection/).
diff --git a/apps/angular/1-projection/jest.config.ts b/apps/angular/1-projection/jest.config.ts
new file mode 100644
index 000000000..fe5eb1627
--- /dev/null
+++ b/apps/angular/1-projection/jest.config.ts
@@ -0,0 +1,23 @@
+/* eslint-disable */
+module.exports = {
+ displayName: 'angular-projection',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ globals: {},
+ coverageDirectory: '../../../coverage/apps/angular/1-projection',
+ 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/angular/1-projection/project.json b/apps/angular/1-projection/project.json
new file mode 100644
index 000000000..42ed1604f
--- /dev/null
+++ b/apps/angular/1-projection/project.json
@@ -0,0 +1,85 @@
+{
+ "name": "angular-projection",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/angular/1-projection/src",
+ "prefix": "app",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/1-projection",
+ "index": "apps/angular/1-projection/src/index.html",
+ "main": "apps/angular/1-projection/src/main.ts",
+ "polyfills": ["apps/angular/1-projection/src/polyfills.ts"],
+ "tsConfig": "apps/angular/1-projection/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/1-projection/src/favicon.ico",
+ "apps/angular/1-projection/src/assets"
+ ],
+ "styles": ["apps/angular/1-projection/src/styles.scss"],
+ "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": {
+ "buildTarget": "angular-projection:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-projection:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-projection:build"
+ }
+ },
+ "test": {
+ "options": {
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "coverage": true
+ }
+ }
+ }
+ }
+}
diff --git a/apps/angular/1-projection/src/app/app.component.ts b/apps/angular/1-projection/src/app/app.component.ts
new file mode 100644
index 000000000..df654bbc2
--- /dev/null
+++ b/apps/angular/1-projection/src/app/app.component.ts
@@ -0,0 +1,17 @@
+import { Component } from '@angular/core';
+import { CityCardComponent } from './component/city-card/city-card.component';
+import { StudentCardComponent } from './component/student-card/student-card.component';
+import { TeacherCardComponent } from './component/teacher-card/teacher-card.component';
+
+@Component({
+ selector: 'app-root',
+ template: `
+
+ `,
+ imports: [TeacherCardComponent, StudentCardComponent, CityCardComponent],
+})
+export class AppComponent {}
diff --git a/apps/angular/1-projection/src/app/component/city-card/city-card.component.ts b/apps/angular/1-projection/src/app/component/city-card/city-card.component.ts
new file mode 100644
index 000000000..8895c8c84
--- /dev/null
+++ b/apps/angular/1-projection/src/app/component/city-card/city-card.component.ts
@@ -0,0 +1,9 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+
+@Component({
+ selector: 'app-city-card',
+ template: 'TODO City',
+ imports: [],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class CityCardComponent {}
diff --git a/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts b/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts
new file mode 100644
index 000000000..bdfa4abd4
--- /dev/null
+++ b/apps/angular/1-projection/src/app/component/student-card/student-card.component.ts
@@ -0,0 +1,40 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ inject,
+ OnInit,
+} from '@angular/core';
+import { FakeHttpService } from '../../data-access/fake-http.service';
+import { StudentStore } from '../../data-access/student.store';
+import { CardType } from '../../model/card.model';
+import { CardComponent } from '../../ui/card/card.component';
+
+@Component({
+ selector: 'app-student-card',
+ template: `
+
+ `,
+ styles: [
+ `
+ ::ng-deep .bg-light-green {
+ background-color: rgba(0, 250, 0, 0.1);
+ }
+ `,
+ ],
+ imports: [CardComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class StudentCardComponent implements OnInit {
+ private http = inject(FakeHttpService);
+ private store = inject(StudentStore);
+
+ students = this.store.students;
+ cardType = CardType.STUDENT;
+
+ ngOnInit(): void {
+ this.http.fetchStudents$.subscribe((s) => this.store.addAll(s));
+ }
+}
diff --git a/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts b/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts
new file mode 100644
index 000000000..adf0ad3c1
--- /dev/null
+++ b/apps/angular/1-projection/src/app/component/teacher-card/teacher-card.component.ts
@@ -0,0 +1,34 @@
+import { Component, inject, OnInit } from '@angular/core';
+import { FakeHttpService } from '../../data-access/fake-http.service';
+import { TeacherStore } from '../../data-access/teacher.store';
+import { CardType } from '../../model/card.model';
+import { CardComponent } from '../../ui/card/card.component';
+
+@Component({
+ selector: 'app-teacher-card',
+ template: `
+
+ `,
+ styles: [
+ `
+ ::ng-deep .bg-light-red {
+ background-color: rgba(250, 0, 0, 0.1);
+ }
+ `,
+ ],
+ imports: [CardComponent],
+})
+export class TeacherCardComponent implements OnInit {
+ private http = inject(FakeHttpService);
+ private store = inject(TeacherStore);
+
+ teachers = this.store.teachers;
+ cardType = CardType.TEACHER;
+
+ ngOnInit(): void {
+ this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t));
+ }
+}
diff --git a/apps/angular/1-projection/src/app/data-access/city.store.ts b/apps/angular/1-projection/src/app/data-access/city.store.ts
new file mode 100644
index 000000000..a8b523569
--- /dev/null
+++ b/apps/angular/1-projection/src/app/data-access/city.store.ts
@@ -0,0 +1,21 @@
+import { Injectable, signal } from '@angular/core';
+import { City } from '../model/city.model';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class CityStore {
+ private cities = signal([]);
+
+ addAll(cities: City[]) {
+ this.cities.set(cities);
+ }
+
+ addOne(city: City) {
+ this.cities.set([...this.cities(), city]);
+ }
+
+ deleteOne(id: number) {
+ this.cities.set(this.cities().filter((s) => s.id !== id));
+ }
+}
diff --git a/apps/projection/src/app/data-access/fake-http.service.ts b/apps/angular/1-projection/src/app/data-access/fake-http.service.ts
similarity index 84%
rename from apps/projection/src/app/data-access/fake-http.service.ts
rename to apps/angular/1-projection/src/app/data-access/fake-http.service.ts
index ac1246746..82a8f1813 100644
--- a/apps/projection/src/app/data-access/fake-http.service.ts
+++ b/apps/angular/1-projection/src/app/data-access/fake-http.service.ts
@@ -12,14 +12,14 @@ import {
import { map, timer } from 'rxjs';
import { City } from '../model/city.model';
import { Student } from '../model/student.model';
-import { subject, Teacher } from '../model/teacher.model';
+import { Teacher, subject } from '../model/teacher.model';
const factoryTeacher = incrementalNumber();
export const randTeacher = () => ({
id: factoryTeacher(),
- firstname: randFirstName(),
- lastname: randLastName(),
+ firstName: randFirstName(),
+ lastName: randLastName(),
subject: rand(subject),
});
@@ -34,9 +34,9 @@ const factoryStudent = incrementalNumber();
export const randStudent = (): Student => ({
id: factoryStudent(),
- firstname: randFirstName(),
- lastname: randLastName(),
- mainTeacher: teachers[randNumber({ max: teachers.length })],
+ firstName: randFirstName(),
+ lastName: randLastName(),
+ mainTeacher: teachers[randNumber({ max: teachers.length - 1 })],
school: randWord(),
});
diff --git a/apps/angular/1-projection/src/app/data-access/student.store.ts b/apps/angular/1-projection/src/app/data-access/student.store.ts
new file mode 100644
index 000000000..6e7f57022
--- /dev/null
+++ b/apps/angular/1-projection/src/app/data-access/student.store.ts
@@ -0,0 +1,21 @@
+import { Injectable, signal } from '@angular/core';
+import { Student } from '../model/student.model';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class StudentStore {
+ public students = signal([]);
+
+ addAll(students: Student[]) {
+ this.students.set(students);
+ }
+
+ addOne(student: Student) {
+ this.students.set([...this.students(), student]);
+ }
+
+ deleteOne(id: number) {
+ this.students.set(this.students().filter((s) => s.id !== id));
+ }
+}
diff --git a/apps/angular/1-projection/src/app/data-access/teacher.store.ts b/apps/angular/1-projection/src/app/data-access/teacher.store.ts
new file mode 100644
index 000000000..5f6dae989
--- /dev/null
+++ b/apps/angular/1-projection/src/app/data-access/teacher.store.ts
@@ -0,0 +1,21 @@
+import { Injectable, signal } from '@angular/core';
+import { Teacher } from '../model/teacher.model';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class TeacherStore {
+ public teachers = signal([]);
+
+ addAll(teachers: Teacher[]) {
+ this.teachers.set(teachers);
+ }
+
+ addOne(teacher: Teacher) {
+ this.teachers.set([...this.teachers(), teacher]);
+ }
+
+ deleteOne(id: number) {
+ this.teachers.set(this.teachers().filter((t) => t.id !== id));
+ }
+}
diff --git a/apps/projection/src/app/model/card.model.ts b/apps/angular/1-projection/src/app/model/card.model.ts
similarity index 100%
rename from apps/projection/src/app/model/card.model.ts
rename to apps/angular/1-projection/src/app/model/card.model.ts
diff --git a/apps/projection/src/app/model/city.model.ts b/apps/angular/1-projection/src/app/model/city.model.ts
similarity index 100%
rename from apps/projection/src/app/model/city.model.ts
rename to apps/angular/1-projection/src/app/model/city.model.ts
diff --git a/apps/angular/1-projection/src/app/model/student.model.ts b/apps/angular/1-projection/src/app/model/student.model.ts
new file mode 100644
index 000000000..bc18e464a
--- /dev/null
+++ b/apps/angular/1-projection/src/app/model/student.model.ts
@@ -0,0 +1,9 @@
+import { Teacher } from './teacher.model';
+
+export interface Student {
+ id: number;
+ firstName: string;
+ lastName: string;
+ mainTeacher: Teacher;
+ school: string;
+}
diff --git a/apps/angular/1-projection/src/app/model/teacher.model.ts b/apps/angular/1-projection/src/app/model/teacher.model.ts
new file mode 100644
index 000000000..34b4241be
--- /dev/null
+++ b/apps/angular/1-projection/src/app/model/teacher.model.ts
@@ -0,0 +1,15 @@
+export const subject = [
+ 'Sciences',
+ 'History',
+ 'English',
+ 'Maths',
+ 'Sport',
+] as const;
+export type Subject = (typeof subject)[number];
+
+export interface Teacher {
+ id: number;
+ firstName: string;
+ lastName: string;
+ subject: Subject;
+}
diff --git a/apps/angular/1-projection/src/app/ui/card/card.component.ts b/apps/angular/1-projection/src/app/ui/card/card.component.ts
new file mode 100644
index 000000000..1a6c3648c
--- /dev/null
+++ b/apps/angular/1-projection/src/app/ui/card/card.component.ts
@@ -0,0 +1,58 @@
+import { NgOptimizedImage } from '@angular/common';
+import { Component, inject, input } from '@angular/core';
+import { randStudent, randTeacher } from '../../data-access/fake-http.service';
+import { StudentStore } from '../../data-access/student.store';
+import { TeacherStore } from '../../data-access/teacher.store';
+import { CardType } from '../../model/card.model';
+import { ListItemComponent } from '../list-item/list-item.component';
+
+@Component({
+ selector: 'app-card',
+ template: `
+
+ @if (type() === CardType.TEACHER) {
+
+ }
+ @if (type() === CardType.STUDENT) {
+
+ }
+
+
+ @for (item of list(); track item) {
+
+ }
+
+
+
+ Add
+
+
+ `,
+ imports: [ListItemComponent, NgOptimizedImage],
+})
+export class CardComponent {
+ private teacherStore = inject(TeacherStore);
+ private studentStore = inject(StudentStore);
+
+ readonly list = input(null);
+ readonly type = input.required();
+ readonly customClass = input('');
+
+ CardType = CardType;
+
+ addNewItem() {
+ const type = this.type();
+ if (type === CardType.TEACHER) {
+ this.teacherStore.addOne(randTeacher());
+ } else if (type === CardType.STUDENT) {
+ this.studentStore.addOne(randStudent());
+ }
+ }
+}
diff --git a/apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts b/apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts
new file mode 100644
index 000000000..5d504f372
--- /dev/null
+++ b/apps/angular/1-projection/src/app/ui/list-item/list-item.component.ts
@@ -0,0 +1,39 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ inject,
+ input,
+} from '@angular/core';
+import { StudentStore } from '../../data-access/student.store';
+import { TeacherStore } from '../../data-access/teacher.store';
+import { CardType } from '../../model/card.model';
+
+@Component({
+ selector: 'app-list-item',
+ template: `
+
+ {{ name() }}
+
+
+
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ListItemComponent {
+ private teacherStore = inject(TeacherStore);
+ private studentStore = inject(StudentStore);
+
+ readonly id = input.required();
+ readonly name = input.required();
+ readonly type = input.required();
+
+ delete(id: number) {
+ const type = this.type();
+ if (type === CardType.TEACHER) {
+ this.teacherStore.deleteOne(id);
+ } else if (type === CardType.STUDENT) {
+ this.studentStore.deleteOne(id);
+ }
+ }
+}
diff --git a/apps/crud/src/assets/.gitkeep b/apps/angular/1-projection/src/assets/.gitkeep
similarity index 100%
rename from apps/crud/src/assets/.gitkeep
rename to apps/angular/1-projection/src/assets/.gitkeep
diff --git a/apps/angular/1-projection/src/assets/img/city.png b/apps/angular/1-projection/src/assets/img/city.png
new file mode 100644
index 000000000..c600f4455
Binary files /dev/null and b/apps/angular/1-projection/src/assets/img/city.png differ
diff --git a/apps/projection/src/assets/img/student.webp b/apps/angular/1-projection/src/assets/img/student.webp
similarity index 100%
rename from apps/projection/src/assets/img/student.webp
rename to apps/angular/1-projection/src/assets/img/student.webp
diff --git a/apps/projection/src/assets/img/teacher.png b/apps/angular/1-projection/src/assets/img/teacher.png
similarity index 100%
rename from apps/projection/src/assets/img/teacher.png
rename to apps/angular/1-projection/src/assets/img/teacher.png
diff --git a/apps/projection/src/assets/svg/trash.svg b/apps/angular/1-projection/src/assets/svg/trash.svg
similarity index 100%
rename from apps/projection/src/assets/svg/trash.svg
rename to apps/angular/1-projection/src/assets/svg/trash.svg
diff --git a/apps/context-outlet-type/src/favicon.ico b/apps/angular/1-projection/src/favicon.ico
similarity index 100%
rename from apps/context-outlet-type/src/favicon.ico
rename to apps/angular/1-projection/src/favicon.ico
diff --git a/apps/angular/1-projection/src/index.html b/apps/angular/1-projection/src/index.html
new file mode 100644
index 000000000..bb9c8bc84
--- /dev/null
+++ b/apps/angular/1-projection/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-projection
+
+
+
+
+
+
+
+
diff --git a/apps/angular/1-projection/src/main.ts b/apps/angular/1-projection/src/main.ts
new file mode 100644
index 000000000..63574fdcf
--- /dev/null
+++ b/apps/angular/1-projection/src/main.ts
@@ -0,0 +1,7 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent, {
+ providers: [provideZoneChangeDetection()],
+}).catch((err) => console.error(err));
diff --git a/apps/context-outlet-type/src/polyfills.ts b/apps/angular/1-projection/src/polyfills.ts
similarity index 100%
rename from apps/context-outlet-type/src/polyfills.ts
rename to apps/angular/1-projection/src/polyfills.ts
diff --git a/apps/angular/1-projection/src/styles.scss b/apps/angular/1-projection/src/styles.scss
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/apps/angular/1-projection/src/styles.scss
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/apps/angular/1-projection/src/test-setup.ts b/apps/angular/1-projection/src/test-setup.ts
new file mode 100644
index 000000000..58c511e08
--- /dev/null
+++ b/apps/angular/1-projection/src/test-setup.ts
@@ -0,0 +1,3 @@
+import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
+
+setupZoneTestEnv();
diff --git a/apps/angular/1-projection/tailwind.config.js b/apps/angular/1-projection/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/angular/1-projection/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/angular/1-projection/tsconfig.app.json b/apps/angular/1-projection/tsconfig.app.json
new file mode 100644
index 000000000..2a1ca1b8d
--- /dev/null
+++ b/apps/angular/1-projection/tsconfig.app.json
@@ -0,0 +1,13 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "target": "ES2022",
+ "useDefineForClassFields": false,
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts", "src/polyfills.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts"]
+}
diff --git a/apps/angular/1-projection/tsconfig.json b/apps/angular/1-projection/tsconfig.json
new file mode 100644
index 000000000..a7033d03a
--- /dev/null
+++ b/apps/angular/1-projection/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ],
+ "compilerOptions": {
+ "target": "es2020",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/1-projection/tsconfig.spec.json b/apps/angular/1-projection/tsconfig.spec.json
new file mode 100644
index 000000000..a26655f5a
--- /dev/null
+++ b/apps/angular/1-projection/tsconfig.spec.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "module": "preserve",
+ "types": ["jest", "node"],
+ "moduleResolution": "bundler",
+ "isolatedModules": true
+ },
+ "files": ["src/test-setup.ts"],
+ "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
+}
diff --git a/apps/angular/10-utility-wrapper-pipe/.eslintrc.json b/apps/angular/10-utility-wrapper-pipe/.eslintrc.json
new file mode 100644
index 000000000..8ebcbfd59
--- /dev/null
+++ b/apps/angular/10-utility-wrapper-pipe/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@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": {}
+ }
+ ]
+}
diff --git a/apps/angular/10-utility-wrapper-pipe/README.md b/apps/angular/10-utility-wrapper-pipe/README.md
new file mode 100644
index 000000000..aac426271
--- /dev/null
+++ b/apps/angular/10-utility-wrapper-pipe/README.md
@@ -0,0 +1,13 @@
+# Utility Wrapper Pipe
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-utility-wrapper-pipe
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/10-pipe-utility/).
diff --git a/apps/angular/10-utility-wrapper-pipe/project.json b/apps/angular/10-utility-wrapper-pipe/project.json
new file mode 100644
index 000000000..37a204043
--- /dev/null
+++ b/apps/angular/10-utility-wrapper-pipe/project.json
@@ -0,0 +1,73 @@
+{
+ "name": "angular-utility-wrapper-pipe",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/angular/10-utility-wrapper-pipe/src",
+ "prefix": "app",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/10-utility-wrapper-pipe",
+ "index": "apps/angular/10-utility-wrapper-pipe/src/index.html",
+ "main": "apps/angular/10-utility-wrapper-pipe/src/main.ts",
+ "polyfills": "apps/angular/10-utility-wrapper-pipe/src/polyfills.ts",
+ "tsConfig": "apps/angular/10-utility-wrapper-pipe/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/10-utility-wrapper-pipe/src/favicon.ico",
+ "apps/angular/10-utility-wrapper-pipe/src/assets"
+ ],
+ "styles": ["apps/angular/10-utility-wrapper-pipe/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": {
+ "buildTarget": "angular-utility-wrapper-pipe:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-utility-wrapper-pipe:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-utility-wrapper-pipe:build"
+ }
+ }
+ }
+}
diff --git a/apps/angular/10-utility-wrapper-pipe/src/app/app.component.ts b/apps/angular/10-utility-wrapper-pipe/src/app/app.component.ts
new file mode 100644
index 000000000..9a8156a5f
--- /dev/null
+++ b/apps/angular/10-utility-wrapper-pipe/src/app/app.component.ts
@@ -0,0 +1,37 @@
+import { Component } from '@angular/core';
+import { PersonUtils } from './person.utils';
+
+@Component({
+ selector: 'app-root',
+ template: `
+ @for (activity of activities; track activity.name) {
+ {{ activity.name }} :
+ @for (
+ person of persons;
+ track person.name;
+ let index = $index;
+ let isFirst = $first
+ ) {
+ {{ showName(person.name, index) }}
+ {{ isAllowed(person.age, isFirst, activity.minimumAge) }}
+ }
+ }
+ `,
+})
+export class AppComponent {
+ persons = [
+ { name: 'Toto', age: 10 },
+ { name: 'Jack', age: 15 },
+ { name: 'John', age: 30 },
+ ];
+
+ activities = [
+ { name: 'biking', minimumAge: 12 },
+ { name: 'hiking', minimumAge: 25 },
+ { name: 'dancing', minimumAge: 1 },
+ ];
+
+ showName = PersonUtils.showName;
+
+ isAllowed = PersonUtils.isAllowed;
+}
diff --git a/apps/pipe-hard/src/app/person.utils.ts b/apps/angular/10-utility-wrapper-pipe/src/app/person.utils.ts
similarity index 100%
rename from apps/pipe-hard/src/app/person.utils.ts
rename to apps/angular/10-utility-wrapper-pipe/src/app/person.utils.ts
diff --git a/apps/declarative-to-reactive/src/assets/.gitkeep b/apps/angular/10-utility-wrapper-pipe/src/assets/.gitkeep
similarity index 100%
rename from apps/declarative-to-reactive/src/assets/.gitkeep
rename to apps/angular/10-utility-wrapper-pipe/src/assets/.gitkeep
diff --git a/apps/crud/src/favicon.ico b/apps/angular/10-utility-wrapper-pipe/src/favicon.ico
similarity index 100%
rename from apps/crud/src/favicon.ico
rename to apps/angular/10-utility-wrapper-pipe/src/favicon.ico
diff --git a/apps/angular/10-utility-wrapper-pipe/src/index.html b/apps/angular/10-utility-wrapper-pipe/src/index.html
new file mode 100644
index 000000000..01d31a163
--- /dev/null
+++ b/apps/angular/10-utility-wrapper-pipe/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-utility-wrapper-pipe
+
+
+
+
+
+
+
+
diff --git a/apps/angular/10-utility-wrapper-pipe/src/main.ts b/apps/angular/10-utility-wrapper-pipe/src/main.ts
new file mode 100644
index 000000000..63574fdcf
--- /dev/null
+++ b/apps/angular/10-utility-wrapper-pipe/src/main.ts
@@ -0,0 +1,7 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent, {
+ providers: [provideZoneChangeDetection()],
+}).catch((err) => console.error(err));
diff --git a/apps/crud/src/polyfills.ts b/apps/angular/10-utility-wrapper-pipe/src/polyfills.ts
similarity index 100%
rename from apps/crud/src/polyfills.ts
rename to apps/angular/10-utility-wrapper-pipe/src/polyfills.ts
diff --git a/apps/context-outlet-type/src/styles.scss b/apps/angular/10-utility-wrapper-pipe/src/styles.scss
similarity index 100%
rename from apps/context-outlet-type/src/styles.scss
rename to apps/angular/10-utility-wrapper-pipe/src/styles.scss
diff --git a/apps/angular/10-utility-wrapper-pipe/tsconfig.app.json b/apps/angular/10-utility-wrapper-pipe/tsconfig.app.json
new file mode 100644
index 000000000..2a1ca1b8d
--- /dev/null
+++ b/apps/angular/10-utility-wrapper-pipe/tsconfig.app.json
@@ -0,0 +1,13 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "target": "ES2022",
+ "useDefineForClassFields": false,
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts", "src/polyfills.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts"]
+}
diff --git a/apps/angular/10-utility-wrapper-pipe/tsconfig.json b/apps/angular/10-utility-wrapper-pipe/tsconfig.json
new file mode 100644
index 000000000..a7033d03a
--- /dev/null
+++ b/apps/angular/10-utility-wrapper-pipe/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ],
+ "compilerOptions": {
+ "target": "es2020",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/13-highly-customizable-css/.eslintrc.json b/apps/angular/13-highly-customizable-css/.eslintrc.json
new file mode 100644
index 000000000..bf8df1428
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/.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/angular/13-highly-customizable-css/README.md b/apps/angular/13-highly-customizable-css/README.md
new file mode 100644
index 000000000..d63171ae6
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/README.md
@@ -0,0 +1,13 @@
+# Highly Customizable CSS
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-highly-customizable-css
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/13-styling/).
diff --git a/apps/angular/13-highly-customizable-css/project.json b/apps/angular/13-highly-customizable-css/project.json
new file mode 100644
index 000000000..c20d3bb48
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/project.json
@@ -0,0 +1,73 @@
+{
+ "name": "angular-highly-customizable-css",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/angular/13-highly-customizable-css/src",
+ "prefix": "app",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/13-highly-customizable-css",
+ "index": "apps/angular/13-highly-customizable-css/src/index.html",
+ "main": "apps/angular/13-highly-customizable-css/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/13-highly-customizable-css/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/13-highly-customizable-css/src/favicon.ico",
+ "apps/angular/13-highly-customizable-css/src/assets"
+ ],
+ "styles": ["apps/angular/13-highly-customizable-css/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": {
+ "buildTarget": "angular-highly-customizable-css:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-highly-customizable-css:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-highly-customizable-css:build"
+ }
+ }
+ }
+}
diff --git a/apps/angular/13-highly-customizable-css/src/app/page.component.ts b/apps/angular/13-highly-customizable-css/src/app/page.component.ts
new file mode 100644
index 000000000..029ca52d2
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/src/app/page.component.ts
@@ -0,0 +1,16 @@
+/* eslint-disable @angular-eslint/component-selector */
+import { Component } from '@angular/core';
+import { TextStaticComponent } from './static-text.component';
+import { TextComponent } from './text.component';
+
+@Component({
+ selector: 'page',
+ imports: [TextStaticComponent, TextComponent],
+ template: `
+
+
+
+ This is a blue text
+ `,
+})
+export class PageComponent {}
diff --git a/apps/angular/13-highly-customizable-css/src/app/static-text.component.ts b/apps/angular/13-highly-customizable-css/src/app/static-text.component.ts
new file mode 100644
index 000000000..703e2a538
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/src/app/static-text.component.ts
@@ -0,0 +1,38 @@
+/* eslint-disable @angular-eslint/component-selector */
+import { Component, computed, input } from '@angular/core';
+import { TextComponent } from './text.component';
+
+export type StaticTextType = 'normal' | 'warning' | 'error';
+
+@Component({
+ selector: 'static-text',
+ imports: [TextComponent],
+ template: `
+ This is a static text
+ `,
+})
+export class TextStaticComponent {
+ type = input('normal');
+
+ font = computed(() => {
+ switch (this.type()) {
+ case 'error':
+ return 30;
+ case 'warning':
+ return 25;
+ default:
+ return 10;
+ }
+ });
+
+ color = computed(() => {
+ switch (this.type()) {
+ case 'error':
+ return 'red';
+ case 'warning':
+ return 'orange';
+ default:
+ return 'black';
+ }
+ });
+}
diff --git a/apps/angular/13-highly-customizable-css/src/app/text.component.ts b/apps/angular/13-highly-customizable-css/src/app/text.component.ts
new file mode 100644
index 000000000..07e3e6255
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/src/app/text.component.ts
@@ -0,0 +1,15 @@
+/* eslint-disable @angular-eslint/component-selector */
+import { Component, input } from '@angular/core';
+
+@Component({
+ selector: 'text',
+ template: `
+
+
+
+ `,
+})
+export class TextComponent {
+ font = input(10);
+ color = input('black');
+}
diff --git a/apps/ngfor-enhancement/src/assets/.gitkeep b/apps/angular/13-highly-customizable-css/src/assets/.gitkeep
similarity index 100%
rename from apps/ngfor-enhancement/src/assets/.gitkeep
rename to apps/angular/13-highly-customizable-css/src/assets/.gitkeep
diff --git a/apps/declarative-to-reactive/src/favicon.ico b/apps/angular/13-highly-customizable-css/src/favicon.ico
similarity index 100%
rename from apps/declarative-to-reactive/src/favicon.ico
rename to apps/angular/13-highly-customizable-css/src/favicon.ico
diff --git a/apps/angular/13-highly-customizable-css/src/index.html b/apps/angular/13-highly-customizable-css/src/index.html
new file mode 100644
index 000000000..e4a84b456
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-highly-customizable-css
+
+
+
+
+
+
+
+
diff --git a/apps/angular/13-highly-customizable-css/src/main.ts b/apps/angular/13-highly-customizable-css/src/main.ts
new file mode 100644
index 000000000..4afd673b4
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/src/main.ts
@@ -0,0 +1,7 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { PageComponent } from './app/page.component';
+
+bootstrapApplication(PageComponent, {
+ providers: [provideZoneChangeDetection()],
+}).catch((err) => console.error(err));
diff --git a/apps/declarative-to-reactive/src/styles.scss b/apps/angular/13-highly-customizable-css/src/styles.scss
similarity index 100%
rename from apps/declarative-to-reactive/src/styles.scss
rename to apps/angular/13-highly-customizable-css/src/styles.scss
diff --git a/apps/angular/13-highly-customizable-css/tsconfig.app.json b/apps/angular/13-highly-customizable-css/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/13-highly-customizable-css/tsconfig.json b/apps/angular/13-highly-customizable-css/tsconfig.json
new file mode 100644
index 000000000..c5655508f
--- /dev/null
+++ b/apps/angular/13-highly-customizable-css/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/16-master-dependency-injection/.eslintrc.json b/apps/angular/16-master-dependency-injection/.eslintrc.json
new file mode 100644
index 000000000..bd4886462
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/.eslintrc.json
@@ -0,0 +1,19 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "rules": {},
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/apps/angular/16-master-dependency-injection/README.md b/apps/angular/16-master-dependency-injection/README.md
new file mode 100644
index 000000000..be19c1ba3
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/README.md
@@ -0,0 +1,13 @@
+# Master Dependancy Injection
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-master-dependency-injection
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/16-di/).
diff --git a/apps/angular/16-master-dependency-injection/project.json b/apps/angular/16-master-dependency-injection/project.json
new file mode 100644
index 000000000..4eb6bd95e
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/project.json
@@ -0,0 +1,75 @@
+{
+ "name": "angular-master-dependency-injection",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/angular/16-master-dependency-injection/src",
+ "prefix": "app",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/16-master-dependency-injection",
+ "index": "apps/angular/16-master-dependency-injection/src/index.html",
+ "main": "apps/angular/16-master-dependency-injection/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/16-master-dependency-injection/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/16-master-dependency-injection/src/favicon.ico",
+ "apps/angular/16-master-dependency-injection/src/assets"
+ ],
+ "styles": [
+ "apps/angular/16-master-dependency-injection/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": {
+ "buildTarget": "angular-master-dependency-injection:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-master-dependency-injection:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-master-dependency-injection:build"
+ }
+ }
+ }
+}
diff --git a/apps/angular/16-master-dependency-injection/src/app/app.component.ts b/apps/angular/16-master-dependency-injection/src/app/app.component.ts
new file mode 100644
index 000000000..332ec9877
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/src/app/app.component.ts
@@ -0,0 +1,54 @@
+import { TableComponent } from '@angular-challenges/shared/ui';
+import { AsyncPipe } from '@angular/common';
+import { ChangeDetectionStrategy, Component, Directive } from '@angular/core';
+import { CurrencyPipe } from './currency.pipe';
+import { CurrencyService } from './currency.service';
+import { Product, products } from './product.model';
+
+interface ProductContext {
+ $implicit: Product;
+}
+
+@Directive({
+ selector: 'ng-template[product]',
+})
+export class ProductDirective {
+ static ngTemplateContextGuard(
+ dir: ProductDirective,
+ ctx: unknown,
+ ): ctx is ProductContext {
+ return true;
+ }
+}
+
+@Component({
+ imports: [TableComponent, CurrencyPipe, AsyncPipe, ProductDirective],
+ providers: [CurrencyService],
+ selector: 'app-root',
+ template: `
+
+
+
+ @for (col of displayedColumns; track $index) {
+
+ {{ col }}
+
+ }
+
+
+
+
+ {{ product.name }}
+ {{ product.priceA | currency | async }}
+ {{ product.priceB | currency | async }}
+ {{ product.priceC | currency | async }}
+
+
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AppComponent {
+ products = products;
+ displayedColumns = ['name', 'priceA', 'priceB', 'priceC'];
+}
diff --git a/apps/angular/16-master-dependency-injection/src/app/currency.pipe.ts b/apps/angular/16-master-dependency-injection/src/app/currency.pipe.ts
new file mode 100644
index 000000000..efa408eb8
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/src/app/currency.pipe.ts
@@ -0,0 +1,16 @@
+import { inject, Pipe, PipeTransform } from '@angular/core';
+import { map } from 'rxjs';
+import { CurrencyService } from './currency.service';
+
+@Pipe({
+ name: 'currency',
+})
+export class CurrencyPipe implements PipeTransform {
+ currencyService = inject(CurrencyService);
+
+ transform(price: number) {
+ return this.currencyService.symbol$.pipe(
+ map((s) => `${String(price)}${s}`),
+ );
+ }
+}
diff --git a/apps/angular/16-master-dependency-injection/src/app/currency.service.ts b/apps/angular/16-master-dependency-injection/src/app/currency.service.ts
new file mode 100644
index 000000000..38b403e48
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/src/app/currency.service.ts
@@ -0,0 +1,29 @@
+import { Injectable } from '@angular/core';
+import { ComponentStore } from '@ngrx/component-store';
+import { map } from 'rxjs';
+
+export interface Currency {
+ name: string;
+ code: string;
+ symbol: string;
+}
+
+export const currency: Currency[] = [
+ { name: 'Euro', code: 'EUR', symbol: '€' },
+ { name: 'Dollar US', code: 'USD', symbol: 'US$' },
+ { name: 'Dollar Autralien', code: 'AUD', symbol: 'AU$' },
+ { name: 'Livre Sterling', code: 'GBP', symbol: '£' },
+ { name: 'Dollar Canadien', code: 'CAD', symbol: 'CAD' },
+];
+
+@Injectable()
+export class CurrencyService extends ComponentStore<{ code: string }> {
+ readonly code$ = this.select((state) => state.code);
+ readonly symbol$ = this.code$.pipe(
+ map((code) => currency.find((c) => c.code === code)?.symbol ?? code),
+ );
+
+ constructor() {
+ super({ code: 'EUR' });
+ }
+}
diff --git a/apps/angular/16-master-dependency-injection/src/app/product.model.ts b/apps/angular/16-master-dependency-injection/src/app/product.model.ts
new file mode 100644
index 000000000..174e7dc77
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/src/app/product.model.ts
@@ -0,0 +1,55 @@
+export interface Product {
+ name: string;
+ priceA: number;
+ priceB: number;
+ priceC: number;
+ currencyCode: string;
+}
+
+export const products: Product[] = [
+ {
+ name: 'bike',
+ priceA: 1000,
+ priceB: 2000,
+ priceC: 2200,
+ currencyCode: 'USD',
+ },
+ { name: 'tent', priceA: 112, priceB: 120, priceC: 41, currencyCode: 'EUR' },
+ {
+ name: 'sofa',
+ priceA: 500,
+ priceB: 422,
+ priceC: 5000,
+ currencyCode: 'EUR',
+ },
+ {
+ name: 'watch',
+ priceA: 50,
+ priceB: 130,
+ priceC: 150,
+ currencyCode: 'AUD',
+ },
+ {
+ name: 'computer',
+ priceA: 1000,
+ priceB: 2200,
+ priceC: 3500,
+ currencyCode: 'GBP',
+ },
+ { name: 'mug', priceA: 10, priceB: 15, priceC: 20, currencyCode: 'EUR' },
+ {
+ name: 'headset',
+ priceA: 100,
+ priceB: 150,
+ priceC: 220,
+ currencyCode: 'CAD',
+ },
+ { name: 'cable', priceA: 5, priceB: 10, priceC: 15, currencyCode: 'EUR' },
+ {
+ name: 'table',
+ priceA: 100,
+ priceB: 20,
+ priceC: 500,
+ currencyCode: 'EUR',
+ },
+];
diff --git a/apps/ngrx-1/src/assets/.gitkeep b/apps/angular/16-master-dependency-injection/src/assets/.gitkeep
similarity index 100%
rename from apps/ngrx-1/src/assets/.gitkeep
rename to apps/angular/16-master-dependency-injection/src/assets/.gitkeep
diff --git a/apps/ngfor-enhancement/src/favicon.ico b/apps/angular/16-master-dependency-injection/src/favicon.ico
similarity index 100%
rename from apps/ngfor-enhancement/src/favicon.ico
rename to apps/angular/16-master-dependency-injection/src/favicon.ico
diff --git a/apps/angular/16-master-dependency-injection/src/index.html b/apps/angular/16-master-dependency-injection/src/index.html
new file mode 100644
index 000000000..be35bf8c8
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-master-dependancy-injection
+
+
+
+
+
+
+
+
diff --git a/apps/angular/16-master-dependency-injection/src/main.ts b/apps/angular/16-master-dependency-injection/src/main.ts
new file mode 100644
index 000000000..63574fdcf
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/src/main.ts
@@ -0,0 +1,7 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent, {
+ providers: [provideZoneChangeDetection()],
+}).catch((err) => console.error(err));
diff --git a/apps/angular/16-master-dependency-injection/src/styles.scss b/apps/angular/16-master-dependency-injection/src/styles.scss
new file mode 100644
index 000000000..bb866699e
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/src/styles.scss
@@ -0,0 +1,21 @@
+table {
+ width: 100%;
+}
+
+table thead > tr > th {
+ text-align: left;
+ padding: 1rem 1rem;
+ border: 1px solid #dee2e6;
+ border-width: 0 0 1px 0;
+ font-weight: 700;
+ color: #343a40;
+ background: #f8f9fa;
+ transition: box-shadow 0.2s;
+}
+
+table tbody > tr > td {
+ text-align: left;
+ border: 1px solid #dee2e6;
+ border-width: 0 0 1px 0;
+ padding: 1rem 1rem;
+}
diff --git a/apps/angular/16-master-dependency-injection/tsconfig.app.json b/apps/angular/16-master-dependency-injection/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/16-master-dependency-injection/tsconfig.json b/apps/angular/16-master-dependency-injection/tsconfig.json
new file mode 100644
index 000000000..c5655508f
--- /dev/null
+++ b/apps/angular/16-master-dependency-injection/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/21-anchor-navigation/.eslintrc.json b/apps/angular/21-anchor-navigation/.eslintrc.json
new file mode 100644
index 000000000..bf8df1428
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/.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/angular/21-anchor-navigation/README.md b/apps/angular/21-anchor-navigation/README.md
new file mode 100644
index 000000000..3683899ba
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/README.md
@@ -0,0 +1,13 @@
+# Anchor Navigation
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-anchor-navigation
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/21-achor-scrolling/).
diff --git a/apps/angular/21-anchor-navigation/jest.config.ts b/apps/angular/21-anchor-navigation/jest.config.ts
new file mode 100644
index 000000000..496a9fe76
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+module.exports = {
+ displayName: 'anchor-navigation-anchor-navigation',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../coverage/apps/angular/21-anchor-navigation',
+ 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/angular/21-anchor-navigation/project.json b/apps/angular/21-anchor-navigation/project.json
new file mode 100644
index 000000000..782bb1ec4
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/project.json
@@ -0,0 +1,84 @@
+{
+ "name": "angular-anchor-navigation",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/angular/21-anchor-navigation/src",
+ "prefix": "app",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/21-anchor-navigation",
+ "index": "apps/angular/21-anchor-navigation/src/index.html",
+ "main": "apps/angular/21-anchor-navigation/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/21-anchor-navigation/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/21-anchor-navigation/src/favicon.ico",
+ "apps/angular/21-anchor-navigation/src/assets"
+ ],
+ "styles": ["apps/angular/21-anchor-navigation/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": {
+ "buildTarget": "angular-anchor-navigation:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-anchor-navigation:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-anchor-navigation:build"
+ }
+ },
+ "test": {
+ "options": {
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "coverage": true
+ }
+ }
+ }
+ }
+}
diff --git a/apps/angular/21-anchor-navigation/src/app/app.component.ts b/apps/angular/21-anchor-navigation/src/app/app.component.ts
new file mode 100644
index 000000000..5caca0271
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/app/app.component.ts
@@ -0,0 +1,11 @@
+import { Component } from '@angular/core';
+import { RouterOutlet } from '@angular/router';
+
+@Component({
+ imports: [RouterOutlet],
+ selector: 'app-root',
+ template: `
+
+ `,
+})
+export class AppComponent {}
diff --git a/apps/angular/21-anchor-navigation/src/app/app.config.ts b/apps/angular/21-anchor-navigation/src/app/app.config.ts
new file mode 100644
index 000000000..66ab7f73f
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/app/app.config.ts
@@ -0,0 +1,6 @@
+import { ApplicationConfig } from '@angular/core';
+import { provideRouter } from '@angular/router';
+import { appRoutes } from './app.routes';
+export const appConfig: ApplicationConfig = {
+ providers: [provideRouter(appRoutes)],
+};
diff --git a/apps/angular/21-anchor-navigation/src/app/app.routes.ts b/apps/angular/21-anchor-navigation/src/app/app.routes.ts
new file mode 100644
index 000000000..e73bb03f3
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/app/app.routes.ts
@@ -0,0 +1,9 @@
+import { Route } from '@angular/router';
+import { FooComponent } from './foo.component';
+import { HomeComponent } from './home.component';
+
+export const appRoutes: Route[] = [
+ { path: 'home', component: HomeComponent },
+ { path: 'foo', component: FooComponent },
+ { path: '**', redirectTo: 'home' },
+];
diff --git a/apps/angular/21-anchor-navigation/src/app/foo.component.ts b/apps/angular/21-anchor-navigation/src/app/foo.component.ts
new file mode 100644
index 000000000..6744c3662
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/app/foo.component.ts
@@ -0,0 +1,14 @@
+import { Component } from '@angular/core';
+import { NavButtonComponent } from './nav-button.component';
+
+@Component({
+ imports: [NavButtonComponent],
+ selector: 'app-foo',
+ template: `
+ Welcome to foo page
+ Home Page
+ section 1
+ section 2
+ `,
+})
+export class FooComponent {}
diff --git a/apps/angular/21-anchor-navigation/src/app/home.component.ts b/apps/angular/21-anchor-navigation/src/app/home.component.ts
new file mode 100644
index 000000000..6ef9bc2b6
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/app/home.component.ts
@@ -0,0 +1,19 @@
+import { Component } from '@angular/core';
+import { NavButtonComponent } from './nav-button.component';
+
+@Component({
+ imports: [NavButtonComponent],
+ selector: 'app-home',
+ template: `
+ Foo Page
+
+ Empty
+ Scroll Bottom
+
+
+ I want to scroll each
+ Scroll Top
+
+ `,
+})
+export class HomeComponent {}
diff --git a/apps/angular/21-anchor-navigation/src/app/nav-button.component.ts b/apps/angular/21-anchor-navigation/src/app/nav-button.component.ts
new file mode 100644
index 000000000..7a22c7f38
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/app/nav-button.component.ts
@@ -0,0 +1,17 @@
+/* eslint-disable @angular-eslint/component-selector */
+import { Component, input } from '@angular/core';
+
+@Component({
+ selector: 'nav-button',
+ template: `
+
+
+
+ `,
+ host: {
+ class: 'block w-fit border border-red-500 rounded-md p-4 m-2',
+ },
+})
+export class NavButtonComponent {
+ href = input('');
+}
diff --git a/apps/ngrx-notification/src/assets/.gitkeep b/apps/angular/21-anchor-navigation/src/assets/.gitkeep
similarity index 100%
rename from apps/ngrx-notification/src/assets/.gitkeep
rename to apps/angular/21-anchor-navigation/src/assets/.gitkeep
diff --git a/apps/ngrx-1/src/favicon.ico b/apps/angular/21-anchor-navigation/src/favicon.ico
similarity index 100%
rename from apps/ngrx-1/src/favicon.ico
rename to apps/angular/21-anchor-navigation/src/favicon.ico
diff --git a/apps/angular/21-anchor-navigation/src/index.html b/apps/angular/21-anchor-navigation/src/index.html
new file mode 100644
index 000000000..06a706a0a
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-anchor-navigation
+
+
+
+
+
+
+
+
diff --git a/apps/angular/21-anchor-navigation/src/main.ts b/apps/angular/21-anchor-navigation/src/main.ts
new file mode 100644
index 000000000..4fcb1ec76
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/main.ts
@@ -0,0 +1,10 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { appConfig } from './app/app.config';
+
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent, {
+ ...appConfig,
+ providers: [provideZoneChangeDetection(), ...appConfig.providers],
+}).catch((err) => console.error(err));
diff --git a/apps/permissions/src/styles.scss b/apps/angular/21-anchor-navigation/src/styles.scss
similarity index 100%
rename from apps/permissions/src/styles.scss
rename to apps/angular/21-anchor-navigation/src/styles.scss
diff --git a/apps/angular/21-anchor-navigation/src/test-setup.ts b/apps/angular/21-anchor-navigation/src/test-setup.ts
new file mode 100644
index 000000000..58c511e08
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/src/test-setup.ts
@@ -0,0 +1,3 @@
+import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
+
+setupZoneTestEnv();
diff --git a/apps/angular/21-anchor-navigation/tailwind.config.js b/apps/angular/21-anchor-navigation/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/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/angular/21-anchor-navigation/tsconfig.app.json b/apps/angular/21-anchor-navigation/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/21-anchor-navigation/tsconfig.json b/apps/angular/21-anchor-navigation/tsconfig.json
new file mode 100644
index 000000000..5010177f7
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/21-anchor-navigation/tsconfig.spec.json b/apps/angular/21-anchor-navigation/tsconfig.spec.json
new file mode 100644
index 000000000..032d49d7a
--- /dev/null
+++ b/apps/angular/21-anchor-navigation/tsconfig.spec.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "module": "preserve",
+ "types": ["jest", "node"],
+ "moduleResolution": "bundler",
+ "isolatedModules": true
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/apps/angular/22-router-input/.eslintrc.json b/apps/angular/22-router-input/.eslintrc.json
new file mode 100644
index 000000000..bf8df1428
--- /dev/null
+++ b/apps/angular/22-router-input/.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/angular/22-router-input/README.md b/apps/angular/22-router-input/README.md
new file mode 100644
index 000000000..0aad6c326
--- /dev/null
+++ b/apps/angular/22-router-input/README.md
@@ -0,0 +1,13 @@
+# @RouterInput()
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-router-input
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/22-router-input/).
diff --git a/apps/angular/22-router-input/project.json b/apps/angular/22-router-input/project.json
new file mode 100644
index 000000000..d0cd43a08
--- /dev/null
+++ b/apps/angular/22-router-input/project.json
@@ -0,0 +1,72 @@
+{
+ "name": "angular-router-input",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/22-router-input/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/22-router-input",
+ "index": "apps/angular/22-router-input/src/index.html",
+ "main": "apps/angular/22-router-input/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/22-router-input/tsconfig.app.json",
+ "assets": [
+ "apps/angular/22-router-input/src/favicon.ico",
+ "apps/angular/22-router-input/src/assets"
+ ],
+ "styles": ["apps/angular/22-router-input/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": {
+ "buildTarget": "angular-router-input:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-router-input:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-router-input:build"
+ }
+ }
+ }
+}
diff --git a/apps/angular/22-router-input/src/app/app.component.ts b/apps/angular/22-router-input/src/app/app.component.ts
new file mode 100644
index 000000000..9dfc11200
--- /dev/null
+++ b/apps/angular/22-router-input/src/app/app.component.ts
@@ -0,0 +1,25 @@
+import { Component } from '@angular/core';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { RouterLink, RouterModule } from '@angular/router';
+
+@Component({
+ imports: [RouterLink, RouterModule, ReactiveFormsModule],
+ selector: 'app-root',
+ template: `
+ UserName
+
+ TestId
+
+
+ Test
+
+ HOME
+
+ `,
+})
+export class AppComponent {
+ userName = new FormControl();
+ testId = new FormControl();
+}
diff --git a/apps/angular/22-router-input/src/app/app.config.ts b/apps/angular/22-router-input/src/app/app.config.ts
new file mode 100644
index 000000000..ed404941f
--- /dev/null
+++ b/apps/angular/22-router-input/src/app/app.config.ts
@@ -0,0 +1,7 @@
+import { ApplicationConfig } from '@angular/core';
+import { provideRouter } from '@angular/router';
+import { appRoutes } from './app.routes';
+
+export const appConfig: ApplicationConfig = {
+ providers: [provideRouter(appRoutes)],
+};
diff --git a/apps/angular/22-router-input/src/app/app.routes.ts b/apps/angular/22-router-input/src/app/app.routes.ts
new file mode 100644
index 000000000..f5d3487c4
--- /dev/null
+++ b/apps/angular/22-router-input/src/app/app.routes.ts
@@ -0,0 +1,15 @@
+import { Route } from '@angular/router';
+
+export const appRoutes: Route[] = [
+ {
+ path: '',
+ loadComponent: () => import('./home.component'),
+ },
+ {
+ path: 'subscription/:testId',
+ loadComponent: () => import('./test.component'),
+ data: {
+ permission: 'admin',
+ },
+ },
+];
diff --git a/apps/angular/22-router-input/src/app/home.component.ts b/apps/angular/22-router-input/src/app/home.component.ts
new file mode 100644
index 000000000..0ddc1501d
--- /dev/null
+++ b/apps/angular/22-router-input/src/app/home.component.ts
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+@Component({
+ selector: 'app-home',
+ imports: [],
+ template: `
+ Home
+ `,
+})
+export default class HomeComponent {}
diff --git a/apps/angular/22-router-input/src/app/test.component.ts b/apps/angular/22-router-input/src/app/test.component.ts
new file mode 100644
index 000000000..747ab4483
--- /dev/null
+++ b/apps/angular/22-router-input/src/app/test.component.ts
@@ -0,0 +1,21 @@
+import { AsyncPipe } from '@angular/common';
+import { Component, inject } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { map } from 'rxjs';
+
+@Component({
+ selector: 'app-subscription',
+ imports: [AsyncPipe],
+ template: `
+ TestId: {{ testId$ | async }}
+ Permission: {{ permission$ | async }}
+ User: {{ user$ | async }}
+ `,
+})
+export default class TestComponent {
+ private activatedRoute = inject(ActivatedRoute);
+
+ testId$ = this.activatedRoute.params.pipe(map((p) => p['testId']));
+ permission$ = this.activatedRoute.data.pipe(map((d) => d['permission']));
+ user$ = this.activatedRoute.queryParams.pipe(map((q) => q['user']));
+}
diff --git a/apps/permissions/src/assets/.gitkeep b/apps/angular/22-router-input/src/assets/.gitkeep
similarity index 100%
rename from apps/permissions/src/assets/.gitkeep
rename to apps/angular/22-router-input/src/assets/.gitkeep
diff --git a/apps/ngrx-notification/src/favicon.ico b/apps/angular/22-router-input/src/favicon.ico
similarity index 100%
rename from apps/ngrx-notification/src/favicon.ico
rename to apps/angular/22-router-input/src/favicon.ico
diff --git a/apps/angular/22-router-input/src/index.html b/apps/angular/22-router-input/src/index.html
new file mode 100644
index 000000000..30e74f7b4
--- /dev/null
+++ b/apps/angular/22-router-input/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-router-input
+
+
+
+
+
+
+
+
diff --git a/apps/angular/22-router-input/src/main.ts b/apps/angular/22-router-input/src/main.ts
new file mode 100644
index 000000000..4919bb4e9
--- /dev/null
+++ b/apps/angular/22-router-input/src/main.ts
@@ -0,0 +1,9 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+import { appConfig } from './app/app.config';
+
+bootstrapApplication(AppComponent, {
+ ...appConfig,
+ providers: [provideZoneChangeDetection(), ...appConfig.providers],
+}).catch((err) => console.error(err));
diff --git a/apps/ngfor-enhancement/src/styles.scss b/apps/angular/22-router-input/src/styles.scss
similarity index 100%
rename from apps/ngfor-enhancement/src/styles.scss
rename to apps/angular/22-router-input/src/styles.scss
diff --git a/apps/angular/22-router-input/tsconfig.app.json b/apps/angular/22-router-input/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/22-router-input/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/22-router-input/tsconfig.json b/apps/angular/22-router-input/tsconfig.json
new file mode 100644
index 000000000..c5655508f
--- /dev/null
+++ b/apps/angular/22-router-input/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/31-module-to-standalone/.eslintrc.json b/apps/angular/31-module-to-standalone/.eslintrc.json
new file mode 100644
index 000000000..bf8df1428
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/.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/angular/31-module-to-standalone/README.md b/apps/angular/31-module-to-standalone/README.md
new file mode 100644
index 000000000..bd227b7a2
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/README.md
@@ -0,0 +1,13 @@
+# Module to Standalone
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-module-to-standalone
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/31-module-to-standalone/).
diff --git a/apps/angular/31-module-to-standalone/project.json b/apps/angular/31-module-to-standalone/project.json
new file mode 100644
index 000000000..b02e0a0a8
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/project.json
@@ -0,0 +1,72 @@
+{
+ "name": "angular-module-to-standalone",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/31-module-to-standalone/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/31-module-to-standalone",
+ "index": "apps/angular/31-module-to-standalone/src/index.html",
+ "main": "apps/angular/31-module-to-standalone/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/31-module-to-standalone/tsconfig.app.json",
+ "assets": [
+ "apps/angular/31-module-to-standalone/src/favicon.ico",
+ "apps/angular/31-module-to-standalone/src/assets"
+ ],
+ "styles": ["apps/angular/31-module-to-standalone/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": {
+ "buildTarget": "angular-module-to-standalone:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-module-to-standalone:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-module-to-standalone:build"
+ }
+ }
+ }
+}
diff --git a/apps/angular/31-module-to-standalone/src/app/app.component.ts b/apps/angular/31-module-to-standalone/src/app/app.component.ts
new file mode 100644
index 000000000..986df84b5
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/src/app/app.component.ts
@@ -0,0 +1,30 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-root',
+ template: `
+
+
+ Home
+
+
+ Admin
+
+
+ User
+
+
+
+ `,
+ host: {
+ class: 'flex flex-col p-4 gap-3',
+ },
+ standalone: false,
+})
+export class AppComponent {}
diff --git a/apps/angular/31-module-to-standalone/src/app/app.module.ts b/apps/angular/31-module-to-standalone/src/app/app.module.ts
new file mode 100644
index 000000000..c795a11b9
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/src/app/app.module.ts
@@ -0,0 +1,11 @@
+import { MainShellModule } from '@angular-challenges/module-to-standalone/shell';
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { AppComponent } from './app.component';
+
+@NgModule({
+ declarations: [AppComponent],
+ imports: [BrowserModule, MainShellModule],
+ bootstrap: [AppComponent],
+})
+export class AppModule {}
diff --git a/apps/pipe-easy/src/assets/.gitkeep b/apps/angular/31-module-to-standalone/src/assets/.gitkeep
similarity index 100%
rename from apps/pipe-easy/src/assets/.gitkeep
rename to apps/angular/31-module-to-standalone/src/assets/.gitkeep
diff --git a/apps/permissions/src/favicon.ico b/apps/angular/31-module-to-standalone/src/favicon.ico
similarity index 100%
rename from apps/permissions/src/favicon.ico
rename to apps/angular/31-module-to-standalone/src/favicon.ico
diff --git a/apps/angular/31-module-to-standalone/src/index.html b/apps/angular/31-module-to-standalone/src/index.html
new file mode 100644
index 000000000..fe0d5b978
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-module-to-standalone
+
+
+
+
+
+
+
+
diff --git a/apps/angular/31-module-to-standalone/src/main.ts b/apps/angular/31-module-to-standalone/src/main.ts
new file mode 100644
index 000000000..8cd87d53b
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/src/main.ts
@@ -0,0 +1,9 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+import { AppModule } from './app/app.module';
+
+platformBrowserDynamic()
+ .bootstrapModule(AppModule, {
+ applicationProviders: [provideZoneChangeDetection()],
+ })
+ .catch((err) => console.error(err));
diff --git a/apps/angular/31-module-to-standalone/src/styles.scss b/apps/angular/31-module-to-standalone/src/styles.scss
new file mode 100644
index 000000000..77e408aa8
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/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/angular/31-module-to-standalone/tailwind.config.js b/apps/angular/31-module-to-standalone/tailwind.config.js
new file mode 100644
index 000000000..99d683dd8
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/tailwind.config.js
@@ -0,0 +1,14 @@
+const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind');
+const { join } = require('path');
+
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ 'apps/module-to-standalone/**/*.{ts,html}',
+ 'libs/module-to-standalone/**/*.{ts,html}',
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+};
diff --git a/apps/angular/31-module-to-standalone/tsconfig.app.json b/apps/angular/31-module-to-standalone/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/31-module-to-standalone/tsconfig.json b/apps/angular/31-module-to-standalone/tsconfig.json
new file mode 100644
index 000000000..c5655508f
--- /dev/null
+++ b/apps/angular/31-module-to-standalone/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/32-change-detection-bug/.eslintrc.json b/apps/angular/32-change-detection-bug/.eslintrc.json
new file mode 100644
index 000000000..bf8df1428
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/.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/angular/32-change-detection-bug/README.md b/apps/angular/32-change-detection-bug/README.md
new file mode 100644
index 000000000..41e533388
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/README.md
@@ -0,0 +1,13 @@
+# Change Detection Bug
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-change-detection-bug
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/performance/32-bug-cd/).
diff --git a/apps/angular/32-change-detection-bug/jest.config.ts b/apps/angular/32-change-detection-bug/jest.config.ts
new file mode 100644
index 000000000..c744feb05
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+module.exports = {
+ displayName: 'angular-change-detection-bug',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../coverage/apps/angular/32-change-detection-bug',
+ 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/angular/32-change-detection-bug/project.json b/apps/angular/32-change-detection-bug/project.json
new file mode 100644
index 000000000..977b76334
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/project.json
@@ -0,0 +1,83 @@
+{
+ "name": "angular-change-detection-bug",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/32-change-detection-bug/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/32-change-detection-bug",
+ "index": "apps/angular/32-change-detection-bug/src/index.html",
+ "main": "apps/angular/32-change-detection-bug/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/32-change-detection-bug/tsconfig.app.json",
+ "assets": [
+ "apps/angular/32-change-detection-bug/src/favicon.ico",
+ "apps/angular/32-change-detection-bug/src/assets"
+ ],
+ "styles": ["apps/angular/32-change-detection-bug/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": {
+ "buildTarget": "angular-change-detection-bug:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-change-detection-bug:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-change-detection-bug:build"
+ }
+ },
+ "test": {
+ "options": {
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "coverage": true
+ }
+ }
+ }
+ }
+}
diff --git a/apps/angular/32-change-detection-bug/src/app/app.component.ts b/apps/angular/32-change-detection-bug/src/app/app.component.ts
new file mode 100644
index 000000000..217999c3a
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/src/app/app.component.ts
@@ -0,0 +1,20 @@
+import { Component } from '@angular/core';
+import { RouterOutlet } from '@angular/router';
+
+@Component({
+ imports: [RouterOutlet],
+ selector: 'app-root',
+ template: `
+ My Application
+
+ `,
+ host: {
+ class: 'flex flex-col gap-2',
+ },
+})
+export class AppComponent {}
diff --git a/apps/angular/32-change-detection-bug/src/app/app.config.ts b/apps/angular/32-change-detection-bug/src/app/app.config.ts
new file mode 100644
index 000000000..0071d5c42
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/src/app/app.config.ts
@@ -0,0 +1,30 @@
+import { ApplicationConfig } from '@angular/core';
+import { provideRouter } from '@angular/router';
+import { BarComponent } from './bar.component';
+import { FooComponent } from './foo.component';
+import { MainNavigationComponent } from './main-navigation.component';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideRouter([
+ {
+ path: '',
+ component: MainNavigationComponent,
+ outlet: 'side',
+ },
+ {
+ path: '',
+ pathMatch: 'full',
+ redirectTo: 'foo',
+ },
+ {
+ path: 'foo',
+ component: FooComponent,
+ },
+ {
+ path: 'bar',
+ component: BarComponent,
+ },
+ ]),
+ ],
+};
diff --git a/apps/angular/32-change-detection-bug/src/app/bar.component.ts b/apps/angular/32-change-detection-bug/src/app/bar.component.ts
new file mode 100644
index 000000000..81981f99d
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/src/app/bar.component.ts
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-bar',
+ template: `
+ BarComponent
+ `,
+})
+export class BarComponent {}
diff --git a/apps/angular/32-change-detection-bug/src/app/fake.service.ts b/apps/angular/32-change-detection-bug/src/app/fake.service.ts
new file mode 100644
index 000000000..7640edfb2
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/src/app/fake.service.ts
@@ -0,0 +1,7 @@
+import { Injectable } from '@angular/core';
+import { delay, of } from 'rxjs';
+
+@Injectable({ providedIn: 'root' })
+export class FakeServiceService {
+ getInfoFromBackend = () => of('Client app').pipe(delay(500));
+}
diff --git a/apps/angular/32-change-detection-bug/src/app/foo.component.ts b/apps/angular/32-change-detection-bug/src/app/foo.component.ts
new file mode 100644
index 000000000..1fcb24326
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/src/app/foo.component.ts
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-foo',
+ template: `
+ Foo Component
+ `,
+})
+export class FooComponent {}
diff --git a/apps/angular/32-change-detection-bug/src/app/main-navigation.component.ts b/apps/angular/32-change-detection-bug/src/app/main-navigation.component.ts
new file mode 100644
index 000000000..1a3a5e93a
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/src/app/main-navigation.component.ts
@@ -0,0 +1,63 @@
+import { Component, inject, input } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { RouterLink, RouterLinkActive } from '@angular/router';
+import { FakeServiceService } from './fake.service';
+
+interface MenuItem {
+ path: string;
+ name: string;
+}
+
+@Component({
+ selector: 'app-nav',
+ imports: [RouterLink, RouterLinkActive],
+ template: `
+ @for (menu of menus(); track menu.path) {
+
+ {{ menu.name }}
+
+ }
+ `,
+ styles: [
+ `
+ a.isSelected {
+ @apply bg-gray-600 text-white;
+ }
+ `,
+ ],
+ host: {
+ class: 'flex flex-col p-2 gap-2',
+ },
+})
+export class NavigationComponent {
+ menus = input.required();
+}
+
+@Component({
+ imports: [NavigationComponent],
+ template: `
+ @if (info() !== null) {
+
+ } @else {
+
+ }
+ `,
+ host: {},
+})
+export class MainNavigationComponent {
+ private fakeBackend = inject(FakeServiceService);
+
+ readonly info = toSignal(this.fakeBackend.getInfoFromBackend(), {
+ initialValue: null,
+ });
+
+ getMenu(prop: string) {
+ return [
+ { path: '/foo', name: `Foo ${prop}` },
+ { path: '/bar', name: `Bar ${prop}` },
+ ];
+ }
+}
diff --git a/apps/pipe-hard/src/assets/.gitkeep b/apps/angular/32-change-detection-bug/src/assets/.gitkeep
similarity index 100%
rename from apps/pipe-hard/src/assets/.gitkeep
rename to apps/angular/32-change-detection-bug/src/assets/.gitkeep
diff --git a/apps/pipe-easy/src/favicon.ico b/apps/angular/32-change-detection-bug/src/favicon.ico
similarity index 100%
rename from apps/pipe-easy/src/favicon.ico
rename to apps/angular/32-change-detection-bug/src/favicon.ico
diff --git a/apps/angular/32-change-detection-bug/src/index.html b/apps/angular/32-change-detection-bug/src/index.html
new file mode 100644
index 000000000..350759387
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-change-detection-bug
+
+
+
+
+
+
+
+
diff --git a/apps/angular/32-change-detection-bug/src/main.ts b/apps/angular/32-change-detection-bug/src/main.ts
new file mode 100644
index 000000000..4919bb4e9
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/src/main.ts
@@ -0,0 +1,9 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+import { appConfig } from './app/app.config';
+
+bootstrapApplication(AppComponent, {
+ ...appConfig,
+ providers: [provideZoneChangeDetection(), ...appConfig.providers],
+}).catch((err) => console.error(err));
diff --git a/apps/angular/32-change-detection-bug/src/styles.scss b/apps/angular/32-change-detection-bug/src/styles.scss
new file mode 100644
index 000000000..77e408aa8
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/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/angular/32-change-detection-bug/src/test-setup.ts b/apps/angular/32-change-detection-bug/src/test-setup.ts
new file mode 100644
index 000000000..9d9196920
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/src/test-setup.ts
@@ -0,0 +1,4 @@
+import '@testing-library/jest-dom';
+import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
+
+setupZoneTestEnv();
diff --git a/apps/angular/32-change-detection-bug/tailwind.config.js b/apps/angular/32-change-detection-bug/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/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/angular/32-change-detection-bug/tsconfig.app.json b/apps/angular/32-change-detection-bug/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/32-change-detection-bug/tsconfig.json b/apps/angular/32-change-detection-bug/tsconfig.json
new file mode 100644
index 000000000..c5655508f
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/32-change-detection-bug/tsconfig.spec.json b/apps/angular/32-change-detection-bug/tsconfig.spec.json
new file mode 100644
index 000000000..1cb322824
--- /dev/null
+++ b/apps/angular/32-change-detection-bug/tsconfig.spec.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "module": "preserve",
+ "types": ["jest", "node", "@testing-library/jest-dom"],
+ "moduleResolution": "bundler",
+ "isolatedModules": true
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/apps/angular/33-decoupling-components/.eslintrc.json b/apps/angular/33-decoupling-components/.eslintrc.json
new file mode 100644
index 000000000..bf8df1428
--- /dev/null
+++ b/apps/angular/33-decoupling-components/.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/angular/33-decoupling-components/README.md b/apps/angular/33-decoupling-components/README.md
new file mode 100644
index 000000000..4af70458f
--- /dev/null
+++ b/apps/angular/33-decoupling-components/README.md
@@ -0,0 +1,13 @@
+# Decoupling Components
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-decoupling-components
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/33-decoupling/).
diff --git a/apps/angular/33-decoupling-components/project.json b/apps/angular/33-decoupling-components/project.json
new file mode 100644
index 000000000..c4140b61b
--- /dev/null
+++ b/apps/angular/33-decoupling-components/project.json
@@ -0,0 +1,72 @@
+{
+ "name": "angular-decoupling-components",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/33-decoupling-components/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/33-decoupling-components",
+ "index": "apps/angular/33-decoupling-components/src/index.html",
+ "main": "apps/angular/33-decoupling-components/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/33-decoupling-components/tsconfig.app.json",
+ "assets": [
+ "apps/angular/33-decoupling-components/src/favicon.ico",
+ "apps/angular/33-decoupling-components/src/assets"
+ ],
+ "styles": ["apps/angular/33-decoupling-components/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": {
+ "buildTarget": "angular-decoupling-components:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-decoupling-components:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-decoupling-components:build"
+ }
+ }
+ }
+}
diff --git a/apps/angular/33-decoupling-components/src/app/app.component.ts b/apps/angular/33-decoupling-components/src/app/app.component.ts
new file mode 100644
index 000000000..0d78f4d34
--- /dev/null
+++ b/apps/angular/33-decoupling-components/src/app/app.component.ts
@@ -0,0 +1,12 @@
+import { BtnDisabledDirective } from '@angular-challenges/decoupling/brain';
+import { BtnHelmetDirective } from '@angular-challenges/decoupling/helmet';
+import { Component } from '@angular/core';
+
+@Component({
+ imports: [BtnDisabledDirective, BtnHelmetDirective],
+ selector: 'app-root',
+ template: `
+ Coucou
+ `,
+})
+export class AppComponent {}
diff --git a/apps/pipe-intermediate/src/assets/.gitkeep b/apps/angular/33-decoupling-components/src/assets/.gitkeep
similarity index 100%
rename from apps/pipe-intermediate/src/assets/.gitkeep
rename to apps/angular/33-decoupling-components/src/assets/.gitkeep
diff --git a/apps/pipe-hard/src/favicon.ico b/apps/angular/33-decoupling-components/src/favicon.ico
similarity index 100%
rename from apps/pipe-hard/src/favicon.ico
rename to apps/angular/33-decoupling-components/src/favicon.ico
diff --git a/apps/angular/33-decoupling-components/src/index.html b/apps/angular/33-decoupling-components/src/index.html
new file mode 100644
index 000000000..b946b0cfd
--- /dev/null
+++ b/apps/angular/33-decoupling-components/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-decoupling-components
+
+
+
+
+
+
+
+
diff --git a/apps/angular/33-decoupling-components/src/main.ts b/apps/angular/33-decoupling-components/src/main.ts
new file mode 100644
index 000000000..63574fdcf
--- /dev/null
+++ b/apps/angular/33-decoupling-components/src/main.ts
@@ -0,0 +1,7 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent, {
+ providers: [provideZoneChangeDetection()],
+}).catch((err) => console.error(err));
diff --git a/apps/angular/33-decoupling-components/src/styles.scss b/apps/angular/33-decoupling-components/src/styles.scss
new file mode 100644
index 000000000..77e408aa8
--- /dev/null
+++ b/apps/angular/33-decoupling-components/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/angular/33-decoupling-components/tailwind.config.js b/apps/angular/33-decoupling-components/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/angular/33-decoupling-components/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/angular/33-decoupling-components/tsconfig.app.json b/apps/angular/33-decoupling-components/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/33-decoupling-components/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/33-decoupling-components/tsconfig.json b/apps/angular/33-decoupling-components/tsconfig.json
new file mode 100644
index 000000000..c5655508f
--- /dev/null
+++ b/apps/angular/33-decoupling-components/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/39-injection-token/.eslintrc.json b/apps/angular/39-injection-token/.eslintrc.json
new file mode 100644
index 000000000..bd4886462
--- /dev/null
+++ b/apps/angular/39-injection-token/.eslintrc.json
@@ -0,0 +1,19 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "rules": {},
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/apps/angular/39-injection-token/README.md b/apps/angular/39-injection-token/README.md
new file mode 100644
index 000000000..a96eb9c6e
--- /dev/null
+++ b/apps/angular/39-injection-token/README.md
@@ -0,0 +1,13 @@
+# InjectionToken
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-injection-token
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/39-injection-token/).
diff --git a/apps/angular/39-injection-token/jest.config.ts b/apps/angular/39-injection-token/jest.config.ts
new file mode 100644
index 000000000..22267ad25
--- /dev/null
+++ b/apps/angular/39-injection-token/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+module.exports = {
+ displayName: 'angular-injection-token',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../coverage/apps/angular/39-injection-token',
+ 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/angular/39-injection-token/project.json b/apps/angular/39-injection-token/project.json
new file mode 100644
index 000000000..135c0c0f6
--- /dev/null
+++ b/apps/angular/39-injection-token/project.json
@@ -0,0 +1,83 @@
+{
+ "name": "angular-injection-token",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/39-injection-token/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/39-injection-token",
+ "index": "apps/angular/39-injection-token/src/index.html",
+ "main": "apps/angular/39-injection-token/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/39-injection-token/tsconfig.app.json",
+ "assets": [
+ "apps/angular/39-injection-token/src/favicon.ico",
+ "apps/angular/39-injection-token/src/assets"
+ ],
+ "styles": ["apps/angular/39-injection-token/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": {
+ "buildTarget": "angular-injection-token:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-injection-token:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-injection-token:build"
+ }
+ },
+ "test": {
+ "options": {
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "coverage": true
+ }
+ }
+ }
+ }
+}
diff --git a/apps/angular/39-injection-token/src/app/app.component.ts b/apps/angular/39-injection-token/src/app/app.component.ts
new file mode 100644
index 000000000..280dc090a
--- /dev/null
+++ b/apps/angular/39-injection-token/src/app/app.component.ts
@@ -0,0 +1,22 @@
+import { Component } from '@angular/core';
+import { RouterLink, RouterOutlet } from '@angular/router';
+
+@Component({
+ imports: [RouterOutlet, RouterLink],
+ selector: 'app-root',
+ template: `
+
+
+ Video
+
+
+ Phone
+
+
+
+ `,
+ host: {
+ class: 'p-10 flex flex-col',
+ },
+})
+export class AppComponent {}
diff --git a/apps/angular/39-injection-token/src/app/app.config.ts b/apps/angular/39-injection-token/src/app/app.config.ts
new file mode 100644
index 000000000..4d6c3cf61
--- /dev/null
+++ b/apps/angular/39-injection-token/src/app/app.config.ts
@@ -0,0 +1,12 @@
+import { ApplicationConfig } from '@angular/core';
+import { provideRouter } from '@angular/router';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideRouter([
+ { path: '', pathMatch: 'full', redirectTo: 'video' },
+ { path: 'video', loadComponent: () => import('./video.component') },
+ { path: 'phone', loadComponent: () => import('./phone.component') },
+ ]),
+ ],
+};
diff --git a/apps/angular/39-injection-token/src/app/data.ts b/apps/angular/39-injection-token/src/app/data.ts
new file mode 100644
index 000000000..393edd0fa
--- /dev/null
+++ b/apps/angular/39-injection-token/src/app/data.ts
@@ -0,0 +1 @@
+export const DEFAULT_TIMER = 1000;
diff --git a/apps/angular/39-injection-token/src/app/phone.component.ts b/apps/angular/39-injection-token/src/app/phone.component.ts
new file mode 100644
index 000000000..41ee3cfc0
--- /dev/null
+++ b/apps/angular/39-injection-token/src/app/phone.component.ts
@@ -0,0 +1,15 @@
+import { Component } from '@angular/core';
+import { TimerContainerComponent } from './timer-container.component';
+
+@Component({
+ selector: 'app-phone',
+ imports: [TimerContainerComponent],
+ template: `
+
+ Phone Call Timer:
+
(should be 2000s)
+
+
+ `,
+})
+export default class PhoneComponent {}
diff --git a/apps/angular/39-injection-token/src/app/timer-container.component.ts b/apps/angular/39-injection-token/src/app/timer-container.component.ts
new file mode 100644
index 000000000..67db6059a
--- /dev/null
+++ b/apps/angular/39-injection-token/src/app/timer-container.component.ts
@@ -0,0 +1,20 @@
+import { Component } from '@angular/core';
+import { DEFAULT_TIMER } from './data';
+import { TimerComponent } from './timer.component';
+@Component({
+ selector: 'timer-container',
+ imports: [TimerComponent],
+ template: `
+
+ Timer container:
+
(timer is {{ timer }}s)
+
+
+ `,
+ host: {
+ class: 'border rounded-md flex p-4 gap-10',
+ },
+})
+export class TimerContainerComponent {
+ timer = DEFAULT_TIMER;
+}
diff --git a/apps/angular/39-injection-token/src/app/timer.component.ts b/apps/angular/39-injection-token/src/app/timer.component.ts
new file mode 100644
index 000000000..335a077bf
--- /dev/null
+++ b/apps/angular/39-injection-token/src/app/timer.component.ts
@@ -0,0 +1,14 @@
+import { Component } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { interval } from 'rxjs';
+import { DEFAULT_TIMER } from './data';
+
+@Component({
+ selector: 'timer',
+ template: `
+ Timer running {{ timer() }}
+ `,
+})
+export class TimerComponent {
+ timer = toSignal(interval(DEFAULT_TIMER));
+}
diff --git a/apps/angular/39-injection-token/src/app/video.component.ts b/apps/angular/39-injection-token/src/app/video.component.ts
new file mode 100644
index 000000000..ba0a218b4
--- /dev/null
+++ b/apps/angular/39-injection-token/src/app/video.component.ts
@@ -0,0 +1,15 @@
+import { Component } from '@angular/core';
+import { TimerContainerComponent } from './timer-container.component';
+
+@Component({
+ selector: 'app-video',
+ imports: [TimerContainerComponent],
+ template: `
+
+ Video Call Timer:
+
(should be the default 1000s)
+
+
+ `,
+})
+export default class VideoComponent {}
diff --git a/apps/projection/src/assets/.gitkeep b/apps/angular/39-injection-token/src/assets/.gitkeep
similarity index 100%
rename from apps/projection/src/assets/.gitkeep
rename to apps/angular/39-injection-token/src/assets/.gitkeep
diff --git a/apps/pipe-intermediate/src/favicon.ico b/apps/angular/39-injection-token/src/favicon.ico
similarity index 100%
rename from apps/pipe-intermediate/src/favicon.ico
rename to apps/angular/39-injection-token/src/favicon.ico
diff --git a/apps/angular/39-injection-token/src/index.html b/apps/angular/39-injection-token/src/index.html
new file mode 100644
index 000000000..3c35787c9
--- /dev/null
+++ b/apps/angular/39-injection-token/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-injection-token
+
+
+
+
+
+
+
+
diff --git a/apps/angular/39-injection-token/src/main.ts b/apps/angular/39-injection-token/src/main.ts
new file mode 100644
index 000000000..4919bb4e9
--- /dev/null
+++ b/apps/angular/39-injection-token/src/main.ts
@@ -0,0 +1,9 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+import { appConfig } from './app/app.config';
+
+bootstrapApplication(AppComponent, {
+ ...appConfig,
+ providers: [provideZoneChangeDetection(), ...appConfig.providers],
+}).catch((err) => console.error(err));
diff --git a/apps/angular/39-injection-token/src/styles.scss b/apps/angular/39-injection-token/src/styles.scss
new file mode 100644
index 000000000..77e408aa8
--- /dev/null
+++ b/apps/angular/39-injection-token/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/angular/39-injection-token/src/test-setup.ts b/apps/angular/39-injection-token/src/test-setup.ts
new file mode 100644
index 000000000..9d9196920
--- /dev/null
+++ b/apps/angular/39-injection-token/src/test-setup.ts
@@ -0,0 +1,4 @@
+import '@testing-library/jest-dom';
+import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
+
+setupZoneTestEnv();
diff --git a/apps/angular/39-injection-token/tailwind.config.js b/apps/angular/39-injection-token/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/angular/39-injection-token/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/angular/39-injection-token/tsconfig.app.json b/apps/angular/39-injection-token/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/39-injection-token/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/39-injection-token/tsconfig.json b/apps/angular/39-injection-token/tsconfig.json
new file mode 100644
index 000000000..5010177f7
--- /dev/null
+++ b/apps/angular/39-injection-token/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/39-injection-token/tsconfig.spec.json b/apps/angular/39-injection-token/tsconfig.spec.json
new file mode 100644
index 000000000..a302b0b88
--- /dev/null
+++ b/apps/angular/39-injection-token/tsconfig.spec.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "module": "preserve",
+ "types": ["jest", "node", "@testing-library/jest-dom"],
+ "moduleResolution": "bundler",
+ "isolatedModules": true
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/apps/angular/4-typed-context-outlet/.eslintrc.json b/apps/angular/4-typed-context-outlet/.eslintrc.json
new file mode 100644
index 000000000..3a7370250
--- /dev/null
+++ b/apps/angular/4-typed-context-outlet/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/apps/angular/4-typed-context-outlet/README.md b/apps/angular/4-typed-context-outlet/README.md
new file mode 100644
index 000000000..e81915167
--- /dev/null
+++ b/apps/angular/4-typed-context-outlet/README.md
@@ -0,0 +1,13 @@
+# Typed ContextOutlet
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-typed-context-outlet
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/4-context-outlet-typed/).
diff --git a/apps/angular/4-typed-context-outlet/project.json b/apps/angular/4-typed-context-outlet/project.json
new file mode 100644
index 000000000..273bca97d
--- /dev/null
+++ b/apps/angular/4-typed-context-outlet/project.json
@@ -0,0 +1,73 @@
+{
+ "name": "angular-typed-context-outlet",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/angular/4-typed-context-outlet/src",
+ "prefix": "app",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/4-typed-context-outlet",
+ "index": "apps/angular/4-typed-context-outlet/src/index.html",
+ "main": "apps/angular/4-typed-context-outlet/src/main.ts",
+ "polyfills": "apps/angular/4-typed-context-outlet/src/polyfills.ts",
+ "tsConfig": "apps/angular/4-typed-context-outlet/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/4-typed-context-outlet/src/favicon.ico",
+ "apps/angular/4-typed-context-outlet/src/assets"
+ ],
+ "styles": ["apps/angular/4-typed-context-outlet/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": {
+ "buildTarget": "angular-typed-context-outlet:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-typed-context-outlet:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-typed-context-outlet:build"
+ }
+ }
+ }
+}
diff --git a/apps/angular/4-typed-context-outlet/src/app/app.component.ts b/apps/angular/4-typed-context-outlet/src/app/app.component.ts
new file mode 100644
index 000000000..d608bec2c
--- /dev/null
+++ b/apps/angular/4-typed-context-outlet/src/app/app.component.ts
@@ -0,0 +1,44 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ListComponent } from './list.component';
+import { PersonComponent } from './person.component';
+
+@Component({
+ imports: [PersonComponent, ListComponent],
+ selector: 'app-root',
+ template: `
+
+
+ {{ name }}: {{ age }}
+
+
+
+
+
+ {{ student.name }}: {{ student.age }} - {{ i }}
+
+
+
+
+
+ {{ city.name }}: {{ city.country }} - {{ i }}
+
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AppComponent {
+ person = {
+ name: 'toto',
+ age: 3,
+ };
+
+ students = [
+ { name: 'toto', age: 3 },
+ { name: 'titi', age: 4 },
+ ];
+
+ cities = [
+ { name: 'Paris', country: 'France' },
+ { name: 'Berlin', country: 'Germany' },
+ ];
+}
diff --git a/apps/angular/4-typed-context-outlet/src/app/list.component.ts b/apps/angular/4-typed-context-outlet/src/app/list.component.ts
new file mode 100644
index 000000000..57fa4e361
--- /dev/null
+++ b/apps/angular/4-typed-context-outlet/src/app/list.component.ts
@@ -0,0 +1,30 @@
+import { NgTemplateOutlet } from '@angular/common';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ contentChild,
+ input,
+ TemplateRef,
+} from '@angular/core';
+
+@Component({
+ selector: 'list',
+ template: `
+ @for (item of list(); track $index) {
+
+ }
+
+ No Template
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [NgTemplateOutlet],
+})
+export class ListComponent {
+ list = input.required();
+
+ listTemplateRef = contentChild('listRef', { read: TemplateRef });
+}
diff --git a/apps/angular/4-typed-context-outlet/src/app/person.component.ts b/apps/angular/4-typed-context-outlet/src/app/person.component.ts
new file mode 100644
index 000000000..d9f5e7520
--- /dev/null
+++ b/apps/angular/4-typed-context-outlet/src/app/person.component.ts
@@ -0,0 +1,21 @@
+import { NgTemplateOutlet } from '@angular/common';
+import { Component, contentChild, input, TemplateRef } from '@angular/core';
+
+@Component({
+ imports: [NgTemplateOutlet],
+ selector: 'person',
+ template: `
+
+
+ No Template
+ `,
+})
+export class PersonComponent {
+ person = input.required<{ name: string; age: number }>();
+
+ personTemplateRef = contentChild('personRef', { read: TemplateRef });
+}
diff --git a/apps/projection/src/favicon.ico b/apps/angular/4-typed-context-outlet/src/favicon.ico
similarity index 100%
rename from apps/projection/src/favicon.ico
rename to apps/angular/4-typed-context-outlet/src/favicon.ico
diff --git a/apps/angular/4-typed-context-outlet/src/index.html b/apps/angular/4-typed-context-outlet/src/index.html
new file mode 100644
index 000000000..a9c0b5484
--- /dev/null
+++ b/apps/angular/4-typed-context-outlet/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-typed-context-outlet
+
+
+
+
+
+
+
+
diff --git a/apps/angular/4-typed-context-outlet/src/main.ts b/apps/angular/4-typed-context-outlet/src/main.ts
new file mode 100644
index 000000000..63574fdcf
--- /dev/null
+++ b/apps/angular/4-typed-context-outlet/src/main.ts
@@ -0,0 +1,7 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent, {
+ providers: [provideZoneChangeDetection()],
+}).catch((err) => console.error(err));
diff --git a/apps/ngfor-enhancement/src/polyfills.ts b/apps/angular/4-typed-context-outlet/src/polyfills.ts
similarity index 100%
rename from apps/ngfor-enhancement/src/polyfills.ts
rename to apps/angular/4-typed-context-outlet/src/polyfills.ts
diff --git a/apps/ngrx-notification/src/styles.scss b/apps/angular/4-typed-context-outlet/src/styles.scss
similarity index 100%
rename from apps/ngrx-notification/src/styles.scss
rename to apps/angular/4-typed-context-outlet/src/styles.scss
diff --git a/apps/angular/4-typed-context-outlet/tsconfig.app.json b/apps/angular/4-typed-context-outlet/tsconfig.app.json
new file mode 100644
index 000000000..2a1ca1b8d
--- /dev/null
+++ b/apps/angular/4-typed-context-outlet/tsconfig.app.json
@@ -0,0 +1,13 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "target": "ES2022",
+ "useDefineForClassFields": false,
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts", "src/polyfills.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts"]
+}
diff --git a/apps/angular/4-typed-context-outlet/tsconfig.json b/apps/angular/4-typed-context-outlet/tsconfig.json
new file mode 100644
index 000000000..a7033d03a
--- /dev/null
+++ b/apps/angular/4-typed-context-outlet/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ],
+ "compilerOptions": {
+ "target": "es2020",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/44-view-transition/.eslintrc.json b/apps/angular/44-view-transition/.eslintrc.json
new file mode 100644
index 000000000..3a7370250
--- /dev/null
+++ b/apps/angular/44-view-transition/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/apps/angular/44-view-transition/README.md b/apps/angular/44-view-transition/README.md
new file mode 100644
index 000000000..02e39b394
--- /dev/null
+++ b/apps/angular/44-view-transition/README.md
@@ -0,0 +1,13 @@
+# View Transition
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-view-transition
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/44-view-transition/).
diff --git a/apps/angular/44-view-transition/project.json b/apps/angular/44-view-transition/project.json
new file mode 100644
index 000000000..4104b5de9
--- /dev/null
+++ b/apps/angular/44-view-transition/project.json
@@ -0,0 +1,70 @@
+{
+ "name": "angular-view-transition",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/44-view-transition/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:application",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/44-view-transition",
+ "index": "apps/angular/44-view-transition/src/index.html",
+ "browser": "apps/angular/44-view-transition/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/44-view-transition/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/44-view-transition/src/favicon.ico",
+ "apps/angular/44-view-transition/src/assets"
+ ],
+ "styles": ["apps/angular/44-view-transition/src/styles.scss"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-view-transition:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-view-transition:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-view-transition:build"
+ }
+ }
+ }
+}
diff --git a/apps/angular/44-view-transition/src/app/app.component.ts b/apps/angular/44-view-transition/src/app/app.component.ts
new file mode 100644
index 000000000..1fcb0c548
--- /dev/null
+++ b/apps/angular/44-view-transition/src/app/app.component.ts
@@ -0,0 +1,12 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { RouterOutlet } from '@angular/router';
+
+@Component({
+ imports: [RouterOutlet],
+ selector: 'app-root',
+ template: `
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AppComponent {}
diff --git a/apps/angular/44-view-transition/src/app/app.config.ts b/apps/angular/44-view-transition/src/app/app.config.ts
new file mode 100644
index 000000000..4c128f040
--- /dev/null
+++ b/apps/angular/44-view-transition/src/app/app.config.ts
@@ -0,0 +1,17 @@
+import { ApplicationConfig } from '@angular/core';
+import { provideRouter, withComponentInputBinding } from '@angular/router';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideRouter(
+ [
+ { path: '', loadComponent: () => import('./blog/blog.component') },
+ {
+ path: 'post/:id',
+ loadComponent: () => import('./post/post.component'),
+ },
+ ],
+ withComponentInputBinding(),
+ ),
+ ],
+};
diff --git a/apps/angular/44-view-transition/src/app/blog/blog.component.ts b/apps/angular/44-view-transition/src/app/blog/blog.component.ts
new file mode 100644
index 000000000..29291d21e
--- /dev/null
+++ b/apps/angular/44-view-transition/src/app/blog/blog.component.ts
@@ -0,0 +1,23 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { posts } from '../data';
+import { ThumbnailComponent } from './thumbnail.component';
+
+@Component({
+ selector: 'blog',
+ imports: [ThumbnailComponent],
+ template: `
+
+ Blog List
+
+
+ @for (post of posts; track post.id) {
+
+ }
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export default class BlogComponent {
+ posts = posts;
+}
diff --git a/apps/angular/44-view-transition/src/app/blog/thumbnail-header.component.ts b/apps/angular/44-view-transition/src/app/blog/thumbnail-header.component.ts
new file mode 100644
index 000000000..18d75aa06
--- /dev/null
+++ b/apps/angular/44-view-transition/src/app/blog/thumbnail-header.component.ts
@@ -0,0 +1,28 @@
+import { NgOptimizedImage } from '@angular/common';
+import { Component, input } from '@angular/core';
+
+@Component({
+ selector: 'thumbnail-header',
+ imports: [NgOptimizedImage],
+ template: `
+
+
+
+ Thomas Laforge
+ {{ date() }}
+
+
+
+ `,
+ host: {
+ class: 'flex w-full px-4 py-5 gap-4 justify-between',
+ },
+})
+export class ThumbnailHeaderComponent {
+ date = input.required();
+}
diff --git a/apps/angular/44-view-transition/src/app/blog/thumbnail.component.ts b/apps/angular/44-view-transition/src/app/blog/thumbnail.component.ts
new file mode 100644
index 000000000..dd2e25e26
--- /dev/null
+++ b/apps/angular/44-view-transition/src/app/blog/thumbnail.component.ts
@@ -0,0 +1,31 @@
+import { NgOptimizedImage } from '@angular/common';
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
+import { RouterLinkWithHref } from '@angular/router';
+import { Post } from '../post.model';
+import { ThumbnailHeaderComponent } from './thumbnail-header.component';
+
+@Component({
+ selector: 'blog-thumbnail',
+ imports: [NgOptimizedImage, ThumbnailHeaderComponent, RouterLinkWithHref],
+ template: `
+
+
+ {{ post().title }}
+ {{ post().description }}
+
+
+ `,
+ host: {
+ class: 'w-full max-w-[600px] rounded-3xl border-none shadow-lg',
+ },
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ThumbnailComponent {
+ post = input.required();
+}
diff --git a/apps/angular/44-view-transition/src/app/data.ts b/apps/angular/44-view-transition/src/app/data.ts
new file mode 100644
index 000000000..52ed38e22
--- /dev/null
+++ b/apps/angular/44-view-transition/src/app/data.ts
@@ -0,0 +1,38 @@
+import { Post } from './post.model';
+
+export const posts: Post[] = [
+ {
+ id: '1',
+ title: 'Future of Change Detection in Angular with Signals',
+ description:
+ 'Learn how change detection is evolving and how signal will improve performance in the future.',
+ image: '/assets/signal-cd.full.webp',
+ date: 'May 23, 2023',
+ readingTime: 5,
+ },
+ {
+ id: '2',
+ title: 'Everything you need to know about route Guard in Angular',
+ description:
+ 'Routing is a significant aspect of any SPA application, and protecting these routes is often necessary. We’ll learn all build-in guards',
+ image: '/assets/guard.full.webp',
+ date: 'Jan 18, 2023',
+ readingTime: 5,
+ },
+ {
+ id: '3',
+ title: 'Create a highly customizable component',
+ description: 'Learn how to create highly customizable component',
+ image: '/assets/highly-custom.full.webp',
+ date: 'Nov 9, 2022',
+ readingTime: 5,
+ },
+];
+
+export const fakeTextChapters = [
+ 'Cyprum itidem insulam procul a continenti discretam et portuosam inter municipia crebra urbes duae faciunt claram Salamis et Paphus, altera Iovis delubris altera Veneris templo insignis. tanta autem tamque multiplici fertilitate abundat rerum omnium eadem Cyprus ut nullius externi indigens adminiculi indigenis viribus a fundamento ipso carinae ad supremos usque carbasos aedificet onerariam navem omnibusque armamentis instructam mari committat',
+ 'Et quoniam mirari posse quosdam peregrinos existimo haec lecturos forsitan, si contigerit, quamobrem cum oratio ad ea monstranda deflexerit quae Romae gererentur, nihil praeter seditiones narratur et tabernas et vilitates harum similis alias, summatim causas perstringam nusquam a veritate sponte propria digressurus',
+ 'Utque aegrum corpus quassari etiam levibus solet offensis, ita animus eius angustus et tener, quicquid increpuisset, ad salutis suae dispendium existimans factum aut cogitatum, insontium caedibus fecit victoriam luctuosam',
+ 'Novo denique perniciosoque exemplo idem Gallus ausus est inire flagitium grave, quod Romae cum ultimo dedecore temptasse aliquando dicitur Gallienus, et adhibitis paucis clam ferro succinctis vesperi per tabernas palabatur et conpita quaeritando Graeco sermone, cuius erat inpendio gnarus, quid de Caesare quisque sentiret. et haec confidenter agebat in urbe ubi pernoctantium luminum claritudo dierum solet imitari fulgorem. postremo agnitus saepe iamque, si prodisset, conspicuum se fore contemplans, non nisi luce palam egrediens ad agenda quae putabat seria cernebatur. et haec quidem medullitus multis gementibus agebantur.',
+ 'Et quoniam mirari posse quosdam peregrinos existimo haec lecturos forsitan, si contigerit, quamobrem cum oratio ad ea monstranda deflexerit quae Romae gererentur, nihil praeter seditiones narratur et tabernas et vilitates harum similis alias, summatim causas perstringam nusquam a veritate sponte propria digressurus.',
+];
diff --git a/apps/angular/44-view-transition/src/app/post.model.ts b/apps/angular/44-view-transition/src/app/post.model.ts
new file mode 100644
index 000000000..232e9efe5
--- /dev/null
+++ b/apps/angular/44-view-transition/src/app/post.model.ts
@@ -0,0 +1,8 @@
+export interface Post {
+ id: string;
+ title: string;
+ description: string;
+ image: string;
+ date: string;
+ readingTime: number;
+}
diff --git a/apps/angular/44-view-transition/src/app/post/post-header.component.ts b/apps/angular/44-view-transition/src/app/post/post-header.component.ts
new file mode 100644
index 000000000..6d5f30e54
--- /dev/null
+++ b/apps/angular/44-view-transition/src/app/post/post-header.component.ts
@@ -0,0 +1,31 @@
+import { NgOptimizedImage } from '@angular/common';
+import { Component, input } from '@angular/core';
+
+@Component({
+ selector: 'post-header',
+ imports: [NgOptimizedImage],
+ template: `
+
+
+
+
+ Thomas Laforge
+ {{ date() }}
+ `,
+ host: {
+ class: 'flex flex-col justify-center items-center',
+ },
+})
+export class PostHeaderComponent {
+ date = input.required();
+}
diff --git a/apps/angular/44-view-transition/src/app/post/post.component.ts b/apps/angular/44-view-transition/src/app/post/post.component.ts
new file mode 100644
index 000000000..edb87f780
--- /dev/null
+++ b/apps/angular/44-view-transition/src/app/post/post.component.ts
@@ -0,0 +1,46 @@
+import { NgOptimizedImage } from '@angular/common';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ input,
+} from '@angular/core';
+import { RouterLink } from '@angular/router';
+import { ThumbnailHeaderComponent } from '../blog/thumbnail-header.component';
+import { fakeTextChapters, posts } from '../data';
+import { PostHeaderComponent } from './post-header.component';
+
+@Component({
+ selector: 'post',
+ imports: [
+ ThumbnailHeaderComponent,
+ NgOptimizedImage,
+ PostHeaderComponent,
+ RouterLink,
+ ],
+ template: `
+
+
+ Back
+
+
+
{{ post().title }}
+
+ @for (chapter of fakeTextChapter; track $index) {
+
{{ chapter }}
+ }
+
+ `,
+ host: {
+ class: 'flex h-full justify-center',
+ },
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export default class PostComponent {
+ id = input.required();
+ post = computed(() => posts.filter((p) => p.id === this.id())[0]);
+
+ fakeTextChapter = fakeTextChapters;
+}
diff --git a/apps/rxjs-pipe-bug/src/assets/.gitkeep b/apps/angular/44-view-transition/src/assets/.gitkeep
similarity index 100%
rename from apps/rxjs-pipe-bug/src/assets/.gitkeep
rename to apps/angular/44-view-transition/src/assets/.gitkeep
diff --git a/apps/angular/44-view-transition/src/assets/angular.webp b/apps/angular/44-view-transition/src/assets/angular.webp
new file mode 100644
index 000000000..b20492845
Binary files /dev/null and b/apps/angular/44-view-transition/src/assets/angular.webp differ
diff --git a/apps/angular/44-view-transition/src/assets/guard.full.webp b/apps/angular/44-view-transition/src/assets/guard.full.webp
new file mode 100644
index 000000000..2f0b4c3d5
Binary files /dev/null and b/apps/angular/44-view-transition/src/assets/guard.full.webp differ
diff --git a/apps/angular/44-view-transition/src/assets/highly-custom.full.webp b/apps/angular/44-view-transition/src/assets/highly-custom.full.webp
new file mode 100644
index 000000000..74c2df8f8
Binary files /dev/null and b/apps/angular/44-view-transition/src/assets/highly-custom.full.webp differ
diff --git a/apps/angular/44-view-transition/src/assets/profil.webp b/apps/angular/44-view-transition/src/assets/profil.webp
new file mode 100644
index 000000000..52adde8fd
Binary files /dev/null and b/apps/angular/44-view-transition/src/assets/profil.webp differ
diff --git a/apps/angular/44-view-transition/src/assets/signal-cd.full.webp b/apps/angular/44-view-transition/src/assets/signal-cd.full.webp
new file mode 100644
index 000000000..e483bbfd5
Binary files /dev/null and b/apps/angular/44-view-transition/src/assets/signal-cd.full.webp differ
diff --git a/apps/rxjs-pipe-bug/src/favicon.ico b/apps/angular/44-view-transition/src/favicon.ico
similarity index 100%
rename from apps/rxjs-pipe-bug/src/favicon.ico
rename to apps/angular/44-view-transition/src/favicon.ico
diff --git a/apps/angular/44-view-transition/src/index.html b/apps/angular/44-view-transition/src/index.html
new file mode 100644
index 000000000..6050d63e5
--- /dev/null
+++ b/apps/angular/44-view-transition/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-view-transition
+
+
+
+
+
+
+
+
diff --git a/apps/angular/44-view-transition/src/main.ts b/apps/angular/44-view-transition/src/main.ts
new file mode 100644
index 000000000..4919bb4e9
--- /dev/null
+++ b/apps/angular/44-view-transition/src/main.ts
@@ -0,0 +1,9 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+import { appConfig } from './app/app.config';
+
+bootstrapApplication(AppComponent, {
+ ...appConfig,
+ providers: [provideZoneChangeDetection(), ...appConfig.providers],
+}).catch((err) => console.error(err));
diff --git a/apps/angular/44-view-transition/src/styles.scss b/apps/angular/44-view-transition/src/styles.scss
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/apps/angular/44-view-transition/src/styles.scss
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/apps/angular/44-view-transition/tailwind.config.js b/apps/angular/44-view-transition/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/angular/44-view-transition/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/angular/44-view-transition/tsconfig.app.json b/apps/angular/44-view-transition/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/44-view-transition/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/44-view-transition/tsconfig.json b/apps/angular/44-view-transition/tsconfig.json
new file mode 100644
index 000000000..1571c2ee6
--- /dev/null
+++ b/apps/angular/44-view-transition/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/45-react-in-angular/.eslintrc.json b/apps/angular/45-react-in-angular/.eslintrc.json
new file mode 100644
index 000000000..8ebcbfd59
--- /dev/null
+++ b/apps/angular/45-react-in-angular/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@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": {}
+ }
+ ]
+}
diff --git a/apps/angular/45-react-in-angular/README.md b/apps/angular/45-react-in-angular/README.md
new file mode 100644
index 000000000..0a8679ac4
--- /dev/null
+++ b/apps/angular/45-react-in-angular/README.md
@@ -0,0 +1,13 @@
+# React in angular
+
+> author: wandrille-guesdon
+
+### Run Application
+
+```bash
+npx nx serve angular-react-in-angular
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/45-react-in-angular/).
diff --git a/apps/angular/45-react-in-angular/jest.config.ts b/apps/angular/45-react-in-angular/jest.config.ts
new file mode 100644
index 000000000..1d0a369b3
--- /dev/null
+++ b/apps/angular/45-react-in-angular/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+module.exports = {
+ displayName: 'angular-react-in-angular',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../coverage/apps/angular/45-react-in-angular',
+ 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/angular/45-react-in-angular/project.json b/apps/angular/45-react-in-angular/project.json
new file mode 100644
index 000000000..8a3bf02fc
--- /dev/null
+++ b/apps/angular/45-react-in-angular/project.json
@@ -0,0 +1,81 @@
+{
+ "name": "angular-react-in-angular",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/45-react-in-angular/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:application",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/45-react-in-angular",
+ "index": "apps/angular/45-react-in-angular/src/index.html",
+ "browser": "apps/angular/45-react-in-angular/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/45-react-in-angular/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/45-react-in-angular/src/favicon.ico",
+ "apps/angular/45-react-in-angular/src/assets"
+ ],
+ "styles": ["apps/angular/45-react-in-angular/src/styles.scss"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-react-in-angular:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-react-in-angular:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-react-in-angular:build"
+ }
+ },
+ "test": {
+ "options": {
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "coverage": true
+ }
+ }
+ }
+ }
+}
diff --git a/apps/angular/45-react-in-angular/src/app/app.component.ts b/apps/angular/45-react-in-angular/src/app/app.component.ts
new file mode 100644
index 000000000..87b9675cc
--- /dev/null
+++ b/apps/angular/45-react-in-angular/src/app/app.component.ts
@@ -0,0 +1,61 @@
+import { Component, signal } from '@angular/core';
+import { PostComponent } from './react/post.component';
+
+type Post = { title: string; description: string };
+
+@Component({
+ imports: [PostComponent],
+ selector: 'app-root',
+ template: `
+
+
+ @for (post of posts; track post.title) {
+
+ }
+
+
+ Selected Post:
+
+ {{ selectedPost()?.title ?? '-' }}
+
+
+
+ `,
+ styles: [''],
+})
+export class AppComponent {
+ readonly posts = [
+ {
+ title: 'A Deep Dive into Angular',
+ description:
+ "Explore Angular's core features, its evolution, and best practices in development for creating dynamic, efficient web applications in our comprehensive guide.",
+ pictureLink:
+ 'https://images.unsplash.com/photo-1471958680802-1345a694ba6d',
+ },
+ {
+ title: 'The Perfect Combination',
+ description:
+ 'Unveil the power of combining Angular & React in web development, maximizing efficiency and flexibility for building scalable, sophisticated applications.',
+ pictureLink:
+ 'https://images.unsplash.com/photo-1518717202715-9fa9d099f58a',
+ },
+ {
+ title: 'Taking Angular to the Next Level',
+ description:
+ "Discover how integrating React with Angular elevates web development, blending Angular's structure with React's UI prowess for advanced applications.",
+ pictureLink:
+ 'https://images.unsplash.com/photo-1532103050105-860af53bc6aa',
+ },
+ ];
+
+ readonly selectedPost = signal(null);
+
+ selectPost(post: Post) {
+ this.selectedPost.set(post);
+ }
+}
diff --git a/apps/angular/45-react-in-angular/src/app/app.config.ts b/apps/angular/45-react-in-angular/src/app/app.config.ts
new file mode 100644
index 000000000..81a6edde4
--- /dev/null
+++ b/apps/angular/45-react-in-angular/src/app/app.config.ts
@@ -0,0 +1,5 @@
+import { ApplicationConfig } from '@angular/core';
+
+export const appConfig: ApplicationConfig = {
+ providers: [],
+};
diff --git a/apps/angular/45-react-in-angular/src/app/react/ReactPost.tsx b/apps/angular/45-react-in-angular/src/app/react/ReactPost.tsx
new file mode 100644
index 000000000..3f6b9e4cd
--- /dev/null
+++ b/apps/angular/45-react-in-angular/src/app/react/ReactPost.tsx
@@ -0,0 +1,29 @@
+// import React from 'react';
+
+export default function ReactPost(props: {
+ title?: string;
+ description?: string;
+ pictureLink?: string;
+ selected?: boolean;
+ handleClick: () => void;
+}) {
+ return (
+
+
+
+
+
{props.title}
+
{props.description}
+
+ Select
+
+
+
+
+ );
+}
diff --git a/apps/angular/45-react-in-angular/src/app/react/post.component.ts b/apps/angular/45-react-in-angular/src/app/react/post.component.ts
new file mode 100644
index 000000000..d5eb2cedf
--- /dev/null
+++ b/apps/angular/45-react-in-angular/src/app/react/post.component.ts
@@ -0,0 +1,16 @@
+import { Component, EventEmitter, input, Output } from '@angular/core';
+
+type Post = { title: string; description: string; pictureLink: string };
+
+@Component({
+ selector: 'app-post',
+ template: `
+
+ `,
+ styles: [''],
+})
+export class PostComponent {
+ post = input(undefined);
+ isSelected = input(false);
+ @Output() selectPost = new EventEmitter();
+}
diff --git a/apps/rxjs-race-condition/src/assets/.gitkeep b/apps/angular/45-react-in-angular/src/assets/.gitkeep
similarity index 100%
rename from apps/rxjs-race-condition/src/assets/.gitkeep
rename to apps/angular/45-react-in-angular/src/assets/.gitkeep
diff --git a/apps/rxjs-race-condition/src/favicon.ico b/apps/angular/45-react-in-angular/src/favicon.ico
similarity index 100%
rename from apps/rxjs-race-condition/src/favicon.ico
rename to apps/angular/45-react-in-angular/src/favicon.ico
diff --git a/apps/angular/45-react-in-angular/src/index.html b/apps/angular/45-react-in-angular/src/index.html
new file mode 100644
index 000000000..9d9dd3923
--- /dev/null
+++ b/apps/angular/45-react-in-angular/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-react-in-angular
+
+
+
+
+
+
+
+
diff --git a/apps/angular/45-react-in-angular/src/main.ts b/apps/angular/45-react-in-angular/src/main.ts
new file mode 100644
index 000000000..4919bb4e9
--- /dev/null
+++ b/apps/angular/45-react-in-angular/src/main.ts
@@ -0,0 +1,9 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+import { appConfig } from './app/app.config';
+
+bootstrapApplication(AppComponent, {
+ ...appConfig,
+ providers: [provideZoneChangeDetection(), ...appConfig.providers],
+}).catch((err) => console.error(err));
diff --git a/apps/angular/45-react-in-angular/src/styles.scss b/apps/angular/45-react-in-angular/src/styles.scss
new file mode 100644
index 000000000..77e408aa8
--- /dev/null
+++ b/apps/angular/45-react-in-angular/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/angular/45-react-in-angular/src/test-setup.ts b/apps/angular/45-react-in-angular/src/test-setup.ts
new file mode 100644
index 000000000..9d9196920
--- /dev/null
+++ b/apps/angular/45-react-in-angular/src/test-setup.ts
@@ -0,0 +1,4 @@
+import '@testing-library/jest-dom';
+import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
+
+setupZoneTestEnv();
diff --git a/apps/angular/45-react-in-angular/tailwind.config.js b/apps/angular/45-react-in-angular/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/angular/45-react-in-angular/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/angular/45-react-in-angular/tsconfig.app.json b/apps/angular/45-react-in-angular/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/45-react-in-angular/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/45-react-in-angular/tsconfig.json b/apps/angular/45-react-in-angular/tsconfig.json
new file mode 100644
index 000000000..64d18756d
--- /dev/null
+++ b/apps/angular/45-react-in-angular/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/45-react-in-angular/tsconfig.spec.json b/apps/angular/45-react-in-angular/tsconfig.spec.json
new file mode 100644
index 000000000..a302b0b88
--- /dev/null
+++ b/apps/angular/45-react-in-angular/tsconfig.spec.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "module": "preserve",
+ "types": ["jest", "node", "@testing-library/jest-dom"],
+ "moduleResolution": "bundler",
+ "isolatedModules": true
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/apps/angular/46-simple-animations/.eslintrc.json b/apps/angular/46-simple-animations/.eslintrc.json
new file mode 100644
index 000000000..8ebcbfd59
--- /dev/null
+++ b/apps/angular/46-simple-animations/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@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": {}
+ }
+ ]
+}
diff --git a/apps/angular/46-simple-animations/README.md b/apps/angular/46-simple-animations/README.md
new file mode 100644
index 000000000..87e6c7e7c
--- /dev/null
+++ b/apps/angular/46-simple-animations/README.md
@@ -0,0 +1,13 @@
+# Simple Animations
+
+> author: sven-brodny
+
+### Run Application
+
+```bash
+npx nx serve angular-simple-animations
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/46-simple-animations/).
diff --git a/apps/angular/46-simple-animations/jest.config.ts b/apps/angular/46-simple-animations/jest.config.ts
new file mode 100644
index 000000000..c058d8afa
--- /dev/null
+++ b/apps/angular/46-simple-animations/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+module.exports = {
+ displayName: 'angular-simple-animations',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../coverage/apps/angular/46-simple-animations',
+ 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/angular/46-simple-animations/project.json b/apps/angular/46-simple-animations/project.json
new file mode 100644
index 000000000..34e44c7b3
--- /dev/null
+++ b/apps/angular/46-simple-animations/project.json
@@ -0,0 +1,81 @@
+{
+ "name": "angular-simple-animations",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/46-simple-animations/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:application",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/46-simple-animations",
+ "index": "apps/angular/46-simple-animations/src/index.html",
+ "browser": "apps/angular/46-simple-animations/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/46-simple-animations/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/46-simple-animations/src/favicon.ico",
+ "apps/angular/46-simple-animations/src/assets"
+ ],
+ "styles": ["apps/angular/46-simple-animations/src/styles.scss"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-simple-animations:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-simple-animations:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-simple-animations:build"
+ }
+ },
+ "test": {
+ "options": {
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "coverage": true
+ }
+ }
+ }
+ }
+}
diff --git a/apps/angular/46-simple-animations/src/app/app.component.ts b/apps/angular/46-simple-animations/src/app/app.component.ts
new file mode 100644
index 000000000..ae63db419
--- /dev/null
+++ b/apps/angular/46-simple-animations/src/app/app.component.ts
@@ -0,0 +1,87 @@
+import { Component } from '@angular/core';
+
+@Component({
+ imports: [],
+ selector: 'app-root',
+ styles: `
+ section {
+ @apply flex flex-1 flex-col gap-5;
+ }
+
+ .list-item {
+ @apply flex flex-row border-b px-5 pb-2;
+
+ span {
+ @apply flex-1;
+ }
+ }
+ `,
+ template: `
+
+
+
+
2008
+
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Vitae
+ mollitia sequi accusantium, distinctio similique laudantium eveniet
+ quidem sit placeat possimus tempore dolorum inventore corporis atque
+ quae ad, nobis explicabo delectus.
+
+
+
+
+
2010
+
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Vitae
+ mollitia sequi accusantium, distinctio similique laudantium eveniet
+ quidem sit placeat possimus tempore dolorum inventore corporis atque
+ quae ad, nobis explicabo delectus.
+
+
+
+
+
2012
+
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Vitae
+ mollitia sequi accusantium, distinctio similique laudantium eveniet
+ quidem sit placeat possimus tempore dolorum inventore corporis atque
+ quae ad, nobis explicabo delectus.
+
+
+
+
+
+
+ Name:
+ Samuel
+
+
+
+ Age:
+ 28
+
+
+
+ Birthdate:
+ 02.11.1995
+
+
+
+ City:
+ Berlin
+
+
+
+ Language:
+ English
+
+
+
+ Like Pizza:
+ Hell yeah
+
+
+
+ `,
+})
+export class AppComponent {}
diff --git a/apps/angular/46-simple-animations/src/app/app.config.ts b/apps/angular/46-simple-animations/src/app/app.config.ts
new file mode 100644
index 000000000..81a6edde4
--- /dev/null
+++ b/apps/angular/46-simple-animations/src/app/app.config.ts
@@ -0,0 +1,5 @@
+import { ApplicationConfig } from '@angular/core';
+
+export const appConfig: ApplicationConfig = {
+ providers: [],
+};
diff --git a/apps/scroll-cd/src/assets/.gitkeep b/apps/angular/46-simple-animations/src/assets/.gitkeep
similarity index 100%
rename from apps/scroll-cd/src/assets/.gitkeep
rename to apps/angular/46-simple-animations/src/assets/.gitkeep
diff --git a/apps/scroll-cd/src/favicon.ico b/apps/angular/46-simple-animations/src/favicon.ico
similarity index 100%
rename from apps/scroll-cd/src/favicon.ico
rename to apps/angular/46-simple-animations/src/favicon.ico
diff --git a/apps/angular/46-simple-animations/src/index.html b/apps/angular/46-simple-animations/src/index.html
new file mode 100644
index 000000000..8fbebf9fb
--- /dev/null
+++ b/apps/angular/46-simple-animations/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-simple-animations
+
+
+
+
+
+
+
+
diff --git a/apps/angular/46-simple-animations/src/main.ts b/apps/angular/46-simple-animations/src/main.ts
new file mode 100644
index 000000000..4919bb4e9
--- /dev/null
+++ b/apps/angular/46-simple-animations/src/main.ts
@@ -0,0 +1,9 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+import { appConfig } from './app/app.config';
+
+bootstrapApplication(AppComponent, {
+ ...appConfig,
+ providers: [provideZoneChangeDetection(), ...appConfig.providers],
+}).catch((err) => console.error(err));
diff --git a/apps/angular/46-simple-animations/src/styles.scss b/apps/angular/46-simple-animations/src/styles.scss
new file mode 100644
index 000000000..77e408aa8
--- /dev/null
+++ b/apps/angular/46-simple-animations/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/angular/46-simple-animations/src/test-setup.ts b/apps/angular/46-simple-animations/src/test-setup.ts
new file mode 100644
index 000000000..9d9196920
--- /dev/null
+++ b/apps/angular/46-simple-animations/src/test-setup.ts
@@ -0,0 +1,4 @@
+import '@testing-library/jest-dom';
+import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
+
+setupZoneTestEnv();
diff --git a/apps/angular/46-simple-animations/tailwind.config.js b/apps/angular/46-simple-animations/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/angular/46-simple-animations/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/angular/46-simple-animations/tsconfig.app.json b/apps/angular/46-simple-animations/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/46-simple-animations/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/46-simple-animations/tsconfig.json b/apps/angular/46-simple-animations/tsconfig.json
new file mode 100644
index 000000000..64d18756d
--- /dev/null
+++ b/apps/angular/46-simple-animations/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/46-simple-animations/tsconfig.spec.json b/apps/angular/46-simple-animations/tsconfig.spec.json
new file mode 100644
index 000000000..a302b0b88
--- /dev/null
+++ b/apps/angular/46-simple-animations/tsconfig.spec.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "module": "preserve",
+ "types": ["jest", "node", "@testing-library/jest-dom"],
+ "moduleResolution": "bundler",
+ "isolatedModules": true
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/apps/angular/5-crud-application/.eslintrc.json b/apps/angular/5-crud-application/.eslintrc.json
new file mode 100644
index 000000000..8ebcbfd59
--- /dev/null
+++ b/apps/angular/5-crud-application/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@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": {}
+ }
+ ]
+}
diff --git a/apps/angular/5-crud-application/README.md b/apps/angular/5-crud-application/README.md
new file mode 100644
index 000000000..8992014e3
--- /dev/null
+++ b/apps/angular/5-crud-application/README.md
@@ -0,0 +1,13 @@
+# Crud application
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-crud-application
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/5-crud/).
diff --git a/apps/angular/5-crud-application/jest.config.ts b/apps/angular/5-crud-application/jest.config.ts
new file mode 100644
index 000000000..6d0974dea
--- /dev/null
+++ b/apps/angular/5-crud-application/jest.config.ts
@@ -0,0 +1,23 @@
+/* eslint-disable */
+module.exports = {
+ displayName: 'angular-crud-application',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ globals: {},
+ coverageDirectory: '../../../coverage/apps/angular/5-crud-application',
+ 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/angular/5-crud-application/project.json b/apps/angular/5-crud-application/project.json
new file mode 100644
index 000000000..a31bd62a7
--- /dev/null
+++ b/apps/angular/5-crud-application/project.json
@@ -0,0 +1,88 @@
+{
+ "name": "angular-crud-application",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/angular/5-crud-application/src",
+ "prefix": "app",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/5-crud-application",
+ "index": "apps/angular/5-crud-application/src/index.html",
+ "main": "apps/angular/5-crud-application/src/main.ts",
+ "polyfills": "apps/angular/5-crud-application/src/polyfills.ts",
+ "tsConfig": "apps/angular/5-crud-application/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/5-crud-application/src/favicon.ico",
+ "apps/angular/5-crud-application/src/assets"
+ ],
+ "styles": [
+ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
+ "apps/angular/5-crud-application/src/styles.scss"
+ ],
+ "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": {
+ "buildTarget": "angular-crud-application:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-crud-application:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-crud-application:build"
+ }
+ },
+ "test": {
+ "options": {
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "coverage": true
+ }
+ }
+ }
+ }
+}
diff --git a/apps/angular/5-crud-application/src/app/app.component.ts b/apps/angular/5-crud-application/src/app/app.component.ts
new file mode 100644
index 000000000..73ba0dc34
--- /dev/null
+++ b/apps/angular/5-crud-application/src/app/app.component.ts
@@ -0,0 +1,49 @@
+import { HttpClient } from '@angular/common/http';
+import { Component, inject, OnInit } from '@angular/core';
+import { randText } from '@ngneat/falso';
+
+@Component({
+ imports: [],
+ selector: 'app-root',
+ template: `
+ @for (todo of todos; track todo.id) {
+ {{ todo.title }}
+ Update
+ }
+ `,
+ styles: [],
+})
+export class AppComponent implements OnInit {
+ private http = inject(HttpClient);
+
+ todos!: any[];
+
+ ngOnInit(): void {
+ this.http
+ .get('https://jsonplaceholder.typicode.com/todos')
+ .subscribe((todos) => {
+ this.todos = todos;
+ });
+ }
+
+ update(todo: any) {
+ this.http
+ .put(
+ `https://jsonplaceholder.typicode.com/todos/${todo.id}`,
+ JSON.stringify({
+ todo: todo.id,
+ title: randText(),
+ body: todo.body,
+ userId: todo.userId,
+ }),
+ {
+ headers: {
+ 'Content-type': 'application/json; charset=UTF-8',
+ },
+ },
+ )
+ .subscribe((todoUpdated: any) => {
+ this.todos[todoUpdated.id - 1] = todoUpdated;
+ });
+ }
+}
diff --git a/apps/angular/5-crud-application/src/app/app.config.ts b/apps/angular/5-crud-application/src/app/app.config.ts
new file mode 100644
index 000000000..1c0c9422f
--- /dev/null
+++ b/apps/angular/5-crud-application/src/app/app.config.ts
@@ -0,0 +1,6 @@
+import { provideHttpClient } from '@angular/common/http';
+import { ApplicationConfig } from '@angular/core';
+
+export const appConfig: ApplicationConfig = {
+ providers: [provideHttpClient()],
+};
diff --git a/tools/generators/.gitkeep b/apps/angular/5-crud-application/src/assets/.gitkeep
similarity index 100%
rename from tools/generators/.gitkeep
rename to apps/angular/5-crud-application/src/assets/.gitkeep
diff --git a/apps/angular/5-crud-application/src/favicon.ico b/apps/angular/5-crud-application/src/favicon.ico
new file mode 100644
index 000000000..317ebcb23
Binary files /dev/null and b/apps/angular/5-crud-application/src/favicon.ico differ
diff --git a/apps/angular/5-crud-application/src/index.html b/apps/angular/5-crud-application/src/index.html
new file mode 100644
index 000000000..b9ec0b609
--- /dev/null
+++ b/apps/angular/5-crud-application/src/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+ angular-crud-application
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/angular/5-crud-application/src/main.ts b/apps/angular/5-crud-application/src/main.ts
new file mode 100644
index 000000000..866d45959
--- /dev/null
+++ b/apps/angular/5-crud-application/src/main.ts
@@ -0,0 +1,10 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { appConfig } from './app/app.config';
+
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent, {
+ ...appConfig,
+ providers: [provideZoneChangeDetection(), ...appConfig.providers],
+}).catch((err) => console.error(err));
diff --git a/apps/ngrx-1/src/polyfills.ts b/apps/angular/5-crud-application/src/polyfills.ts
similarity index 100%
rename from apps/ngrx-1/src/polyfills.ts
rename to apps/angular/5-crud-application/src/polyfills.ts
diff --git a/apps/crud/src/styles.scss b/apps/angular/5-crud-application/src/styles.scss
similarity index 100%
rename from apps/crud/src/styles.scss
rename to apps/angular/5-crud-application/src/styles.scss
diff --git a/apps/angular/5-crud-application/src/test-setup.ts b/apps/angular/5-crud-application/src/test-setup.ts
new file mode 100644
index 000000000..9d9196920
--- /dev/null
+++ b/apps/angular/5-crud-application/src/test-setup.ts
@@ -0,0 +1,4 @@
+import '@testing-library/jest-dom';
+import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
+
+setupZoneTestEnv();
diff --git a/apps/angular/5-crud-application/tsconfig.app.json b/apps/angular/5-crud-application/tsconfig.app.json
new file mode 100644
index 000000000..2a1ca1b8d
--- /dev/null
+++ b/apps/angular/5-crud-application/tsconfig.app.json
@@ -0,0 +1,13 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "target": "ES2022",
+ "useDefineForClassFields": false,
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts", "src/polyfills.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts"]
+}
diff --git a/apps/angular/5-crud-application/tsconfig.json b/apps/angular/5-crud-application/tsconfig.json
new file mode 100644
index 000000000..a7033d03a
--- /dev/null
+++ b/apps/angular/5-crud-application/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ],
+ "compilerOptions": {
+ "target": "es2020",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/5-crud-application/tsconfig.spec.json b/apps/angular/5-crud-application/tsconfig.spec.json
new file mode 100644
index 000000000..ae7e867cb
--- /dev/null
+++ b/apps/angular/5-crud-application/tsconfig.spec.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "module": "preserve",
+ "types": ["jest", "node", "@testing-library/jest-dom"],
+ "moduleResolution": "bundler",
+ "isolatedModules": true
+ },
+ "files": ["src/test-setup.ts"],
+ "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
+}
diff --git a/apps/angular/52-lazy-load-component/.eslintrc.json b/apps/angular/52-lazy-load-component/.eslintrc.json
new file mode 100644
index 000000000..8ebcbfd59
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@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": {}
+ }
+ ]
+}
diff --git a/apps/angular/52-lazy-load-component/README.md b/apps/angular/52-lazy-load-component/README.md
new file mode 100644
index 000000000..7da25f89b
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/README.md
@@ -0,0 +1,13 @@
+# lazy-load-component
+
+> author: lance-finney
+
+### Run Application
+
+```bash
+npx nx serve angular-lazy-load-component
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/52-lazy-load-component/).
diff --git a/apps/angular/52-lazy-load-component/jest.config.ts b/apps/angular/52-lazy-load-component/jest.config.ts
new file mode 100644
index 000000000..319c30f9f
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+module.exports = {
+ displayName: 'angular-lazy-load-component',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../coverage/apps/angular/52-lazy-load-component',
+ 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/angular/52-lazy-load-component/project.json b/apps/angular/52-lazy-load-component/project.json
new file mode 100644
index 000000000..933dd17e5
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/project.json
@@ -0,0 +1,81 @@
+{
+ "name": "angular-lazy-load-component",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/52-lazy-load-component/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:application",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/52-lazy-load-component",
+ "index": "apps/angular/52-lazy-load-component/src/index.html",
+ "browser": "apps/angular/52-lazy-load-component/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/52-lazy-load-component/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/52-lazy-load-component/src/favicon.ico",
+ "apps/angular/52-lazy-load-component/src/assets"
+ ],
+ "styles": ["apps/angular/52-lazy-load-component/src/styles.scss"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-lazy-load-component:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-lazy-load-component:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-lazy-load-component:build"
+ }
+ },
+ "test": {
+ "options": {
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "coverage": true
+ }
+ }
+ }
+ }
+}
diff --git a/apps/angular/52-lazy-load-component/src/app/app.component.ts b/apps/angular/52-lazy-load-component/src/app/app.component.ts
new file mode 100644
index 000000000..6d8c03d29
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/src/app/app.component.ts
@@ -0,0 +1,23 @@
+import { Component, signal } from '@angular/core';
+
+@Component({
+ selector: 'app-root',
+ template: `
+
+ @if (topLoaded()) {
+
+ } @else {
+
+
+ Load Top
+
+ }
+
+ `,
+ standalone: false,
+})
+export class AppComponent {
+ topLoaded = signal(false);
+}
diff --git a/apps/angular/52-lazy-load-component/src/app/app.module.ts b/apps/angular/52-lazy-load-component/src/app/app.module.ts
new file mode 100644
index 000000000..b5d430e67
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/src/app/app.module.ts
@@ -0,0 +1,12 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { AppComponent } from './app.component';
+import { PlaceholderComponent } from './placeholder.component';
+import { TopComponent } from './top.component';
+
+@NgModule({
+ declarations: [AppComponent, PlaceholderComponent, TopComponent],
+ imports: [BrowserModule],
+ bootstrap: [AppComponent],
+})
+export class AppModule {}
diff --git a/apps/angular/52-lazy-load-component/src/app/placeholder.component.ts b/apps/angular/52-lazy-load-component/src/app/placeholder.component.ts
new file mode 100644
index 000000000..cbb2b5fa6
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/src/app/placeholder.component.ts
@@ -0,0 +1,18 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-placeholder',
+ template: `
+ I'm a placeholder component.
+ `,
+ styles: `
+ :host {
+ display: grid;
+ padding: 20px;
+ background-color: #f0f0f0;
+ height: 50%;
+ }
+ `,
+ standalone: false,
+})
+export class PlaceholderComponent {}
diff --git a/apps/angular/52-lazy-load-component/src/app/top.component.ts b/apps/angular/52-lazy-load-component/src/app/top.component.ts
new file mode 100644
index 000000000..e1ca9012c
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/src/app/top.component.ts
@@ -0,0 +1,18 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-top',
+ template: `
+ I am a very heavy, expensive component that should be lazy loaded.
+ `,
+ styles: `
+ :host {
+ display: grid;
+ padding: 20px;
+ background-color: #f0f0f0;
+ height: 50%;
+ }
+ `,
+ standalone: false,
+})
+export class TopComponent {}
diff --git a/NX b/apps/angular/52-lazy-load-component/src/assets/.gitkeep
similarity index 100%
rename from NX
rename to apps/angular/52-lazy-load-component/src/assets/.gitkeep
diff --git a/apps/angular/52-lazy-load-component/src/favicon.ico b/apps/angular/52-lazy-load-component/src/favicon.ico
new file mode 100644
index 000000000..317ebcb23
Binary files /dev/null and b/apps/angular/52-lazy-load-component/src/favicon.ico differ
diff --git a/apps/angular/52-lazy-load-component/src/index.html b/apps/angular/52-lazy-load-component/src/index.html
new file mode 100644
index 000000000..242ec3e3f
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-lazy-load-component
+
+
+
+
+
+
+
+
diff --git a/apps/angular/52-lazy-load-component/src/main.ts b/apps/angular/52-lazy-load-component/src/main.ts
new file mode 100644
index 000000000..8cd87d53b
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/src/main.ts
@@ -0,0 +1,9 @@
+import { provideZoneChangeDetection } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+import { AppModule } from './app/app.module';
+
+platformBrowserDynamic()
+ .bootstrapModule(AppModule, {
+ applicationProviders: [provideZoneChangeDetection()],
+ })
+ .catch((err) => console.error(err));
diff --git a/apps/angular/52-lazy-load-component/src/styles.scss b/apps/angular/52-lazy-load-component/src/styles.scss
new file mode 100644
index 000000000..77e408aa8
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/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/angular/52-lazy-load-component/src/test-setup.ts b/apps/angular/52-lazy-load-component/src/test-setup.ts
new file mode 100644
index 000000000..9d9196920
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/src/test-setup.ts
@@ -0,0 +1,4 @@
+import '@testing-library/jest-dom';
+import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
+
+setupZoneTestEnv();
diff --git a/apps/angular/52-lazy-load-component/tailwind.config.js b/apps/angular/52-lazy-load-component/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/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/angular/52-lazy-load-component/tsconfig.app.json b/apps/angular/52-lazy-load-component/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/52-lazy-load-component/tsconfig.json b/apps/angular/52-lazy-load-component/tsconfig.json
new file mode 100644
index 000000000..64d18756d
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/52-lazy-load-component/tsconfig.spec.json b/apps/angular/52-lazy-load-component/tsconfig.spec.json
new file mode 100644
index 000000000..a302b0b88
--- /dev/null
+++ b/apps/angular/52-lazy-load-component/tsconfig.spec.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "module": "preserve",
+ "types": ["jest", "node", "@testing-library/jest-dom"],
+ "moduleResolution": "bundler",
+ "isolatedModules": true
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/apps/angular/55-back-button-navigation/.eslintrc.json b/apps/angular/55-back-button-navigation/.eslintrc.json
new file mode 100644
index 000000000..8ebcbfd59
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@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": {}
+ }
+ ]
+}
diff --git a/apps/angular/55-back-button-navigation/README.md b/apps/angular/55-back-button-navigation/README.md
new file mode 100644
index 000000000..2534d270c
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/README.md
@@ -0,0 +1,13 @@
+# Back-Button-Navigation
+
+> author: ioannis-tsironis
+
+### Run Application
+
+```bash
+npx nx serve angular-back-button-navigation
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/55-back-button-navigation/).
diff --git a/apps/angular/55-back-button-navigation/jest.config.ts b/apps/angular/55-back-button-navigation/jest.config.ts
new file mode 100644
index 000000000..f6353cea0
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+module.exports = {
+ displayName: 'angular-back-button-navigation',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../coverage/apps/angular/55-back-button-navigation',
+ 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/angular/55-back-button-navigation/project.json b/apps/angular/55-back-button-navigation/project.json
new file mode 100644
index 000000000..e6c824729
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/project.json
@@ -0,0 +1,86 @@
+{
+ "name": "angular-back-button-navigation",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/55-back-button-navigation/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:application",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/55-back-button-navigation",
+ "index": "apps/angular/55-back-button-navigation/src/index.html",
+ "browser": "apps/angular/55-back-button-navigation/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/55-back-button-navigation/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "apps/angular/55-back-button-navigation/public"
+ }
+ ],
+ "styles": [
+ "apps/angular/55-back-button-navigation/src/styles.scss",
+ "node_modules/@angular/material/prebuilt-themes/indigo-pink.css"
+ ],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-back-button-navigation:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-back-button-navigation:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-back-button-navigation:build"
+ }
+ },
+ "test": {
+ "options": {
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "coverage": true
+ }
+ }
+ }
+ }
+}
diff --git a/apps/angular/55-back-button-navigation/src/app/app.component.html b/apps/angular/55-back-button-navigation/src/app/app.component.html
new file mode 100644
index 000000000..0680b43f9
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/app/app.component.html
@@ -0,0 +1 @@
+
diff --git a/apps/angular/55-back-button-navigation/src/app/app.component.ts b/apps/angular/55-back-button-navigation/src/app/app.component.ts
new file mode 100644
index 000000000..baffdae25
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/app/app.component.ts
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+import { RouterLink, RouterOutlet } from '@angular/router';
+
+@Component({
+ imports: [RouterOutlet, RouterLink],
+ selector: 'app-root',
+ templateUrl: './app.component.html',
+})
+export class AppComponent {}
diff --git a/apps/angular/55-back-button-navigation/src/app/app.config.ts b/apps/angular/55-back-button-navigation/src/app/app.config.ts
new file mode 100644
index 000000000..440cdf2c3
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/app/app.config.ts
@@ -0,0 +1,10 @@
+import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
+import { provideRouter } from '@angular/router';
+import { APP_ROUTES } from './app.routes';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideZoneChangeDetection({ eventCoalescing: true }),
+ provideRouter(APP_ROUTES),
+ ],
+};
diff --git a/apps/angular/55-back-button-navigation/src/app/app.routes.ts b/apps/angular/55-back-button-navigation/src/app/app.routes.ts
new file mode 100644
index 000000000..7deecd57a
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/app/app.routes.ts
@@ -0,0 +1,24 @@
+import { Routes } from '@angular/router';
+import { HomeComponent } from './home/home.component';
+import { SensitiveActionComponent } from './sensitive-action/sensitive-action.component';
+import { SimpleActionComponent } from './simple-action/simple-action.component';
+
+export const APP_ROUTES: Routes = [
+ {
+ path: '',
+ pathMatch: 'full',
+ redirectTo: 'home',
+ },
+ {
+ path: 'home',
+ component: HomeComponent,
+ },
+ {
+ path: 'simple-action',
+ component: SimpleActionComponent,
+ },
+ {
+ path: 'sensitive-action',
+ component: SensitiveActionComponent,
+ },
+];
diff --git a/apps/angular/55-back-button-navigation/src/app/dialog/dialog.component.html b/apps/angular/55-back-button-navigation/src/app/dialog/dialog.component.html
new file mode 100644
index 000000000..ff00ea965
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/app/dialog/dialog.component.html
@@ -0,0 +1,6 @@
+Delete file
+Would you like to delete cat.jpeg?
+
+ No
+ Ok
+
diff --git a/apps/angular/55-back-button-navigation/src/app/dialog/dialog.component.ts b/apps/angular/55-back-button-navigation/src/app/dialog/dialog.component.ts
new file mode 100644
index 000000000..9a9dd0fef
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/app/dialog/dialog.component.ts
@@ -0,0 +1,25 @@
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
+import {
+ MatDialogActions,
+ MatDialogClose,
+ MatDialogContent,
+ MatDialogRef,
+ MatDialogTitle,
+} from '@angular/material/dialog';
+
+@Component({
+ selector: 'app-dialog-dialog',
+ templateUrl: './dialog.component.html',
+ imports: [
+ MatButtonModule,
+ MatDialogActions,
+ MatDialogClose,
+ MatDialogTitle,
+ MatDialogContent,
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DialogComponent {
+ readonly dialogRef = inject(MatDialogRef);
+}
diff --git a/apps/angular/55-back-button-navigation/src/app/home/home.component.html b/apps/angular/55-back-button-navigation/src/app/home/home.component.html
new file mode 100644
index 000000000..cce9e6d4f
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/app/home/home.component.html
@@ -0,0 +1,7 @@
+
+ Go to simple dialog action page
+
+
+
+ Go to sensitive dialog action page
+
diff --git a/apps/angular/55-back-button-navigation/src/app/home/home.component.ts b/apps/angular/55-back-button-navigation/src/app/home/home.component.ts
new file mode 100644
index 000000000..18c4147b1
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/app/home/home.component.ts
@@ -0,0 +1,10 @@
+import { Component } from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
+import { RouterLink } from '@angular/router';
+
+@Component({
+ imports: [MatButtonModule, RouterLink],
+ selector: 'app-home',
+ templateUrl: './home.component.html',
+})
+export class HomeComponent {}
diff --git a/apps/angular/55-back-button-navigation/src/app/sensitive-action/sensitive-action.component.html b/apps/angular/55-back-button-navigation/src/app/sensitive-action/sensitive-action.component.html
new file mode 100644
index 000000000..bcb7382e9
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/app/sensitive-action/sensitive-action.component.html
@@ -0,0 +1,3 @@
+
+ Open dialog with confirmation dialog on browser back button click
+
diff --git a/apps/angular/55-back-button-navigation/src/app/sensitive-action/sensitive-action.component.ts b/apps/angular/55-back-button-navigation/src/app/sensitive-action/sensitive-action.component.ts
new file mode 100644
index 000000000..a97282c33
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/app/sensitive-action/sensitive-action.component.ts
@@ -0,0 +1,19 @@
+import { Component, inject } from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
+import { MatDialog } from '@angular/material/dialog';
+import { DialogComponent } from '../dialog/dialog.component';
+
+@Component({
+ imports: [MatButtonModule],
+ selector: 'app-sensitive-action',
+ templateUrl: './sensitive-action.component.html',
+})
+export class SensitiveActionComponent {
+ readonly #dialog = inject(MatDialog);
+
+ openDialog(): void {
+ this.#dialog.open(DialogComponent, {
+ width: '250px',
+ });
+ }
+}
diff --git a/apps/angular/55-back-button-navigation/src/app/simple-action/simple-action.component.html b/apps/angular/55-back-button-navigation/src/app/simple-action/simple-action.component.html
new file mode 100644
index 000000000..95f63e65e
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/app/simple-action/simple-action.component.html
@@ -0,0 +1 @@
+Open simple dialog
diff --git a/apps/angular/55-back-button-navigation/src/app/simple-action/simple-action.component.ts b/apps/angular/55-back-button-navigation/src/app/simple-action/simple-action.component.ts
new file mode 100644
index 000000000..fe97e7368
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/app/simple-action/simple-action.component.ts
@@ -0,0 +1,19 @@
+import { Component, inject } from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
+import { MatDialog } from '@angular/material/dialog';
+import { DialogComponent } from '../dialog/dialog.component';
+
+@Component({
+ imports: [MatButtonModule],
+ selector: 'app-simple-action',
+ templateUrl: './simple-action.component.html',
+})
+export class SimpleActionComponent {
+ readonly #dialog = inject(MatDialog);
+
+ openDialog(): void {
+ this.#dialog.open(DialogComponent, {
+ width: '250px',
+ });
+ }
+}
diff --git a/apps/angular/55-back-button-navigation/src/index.html b/apps/angular/55-back-button-navigation/src/index.html
new file mode 100644
index 000000000..4e657d614
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-back-button-navigation
+
+
+
+
+
+
+
+
diff --git a/apps/angular/55-back-button-navigation/src/main.ts b/apps/angular/55-back-button-navigation/src/main.ts
new file mode 100644
index 000000000..f3a7223da
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/main.ts
@@ -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),
+);
diff --git a/apps/angular/55-back-button-navigation/src/styles.scss b/apps/angular/55-back-button-navigation/src/styles.scss
new file mode 100644
index 000000000..acd290007
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/styles.scss
@@ -0,0 +1,29 @@
+@use '@angular/material' as mat;
+
+/* You can add global styles to this file, and also import other style files */
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@include mat.elevation-classes();
+@include mat.app-background();
+
+$theme-primary: mat.m2-define-palette(mat.$m2-indigo-palette);
+$theme-accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400);
+
+$theme-warn: mat.m2-define-palette(mat.$m2-red-palette);
+
+$theme: mat.m2-define-light-theme(
+ (
+ color: (
+ primary: $theme-primary,
+ accent: $theme-accent,
+ warn: $theme-warn,
+ ),
+ typography: mat.m2-define-typography-config(),
+ )
+);
+
+@include mat.dialog-theme($theme);
+@include mat.button-theme($theme);
diff --git a/apps/angular/55-back-button-navigation/src/test-setup.ts b/apps/angular/55-back-button-navigation/src/test-setup.ts
new file mode 100644
index 000000000..9d9196920
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/src/test-setup.ts
@@ -0,0 +1,4 @@
+import '@testing-library/jest-dom';
+import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
+
+setupZoneTestEnv();
diff --git a/apps/angular/55-back-button-navigation/tailwind.config.js b/apps/angular/55-back-button-navigation/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/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/angular/55-back-button-navigation/tsconfig.app.json b/apps/angular/55-back-button-navigation/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/55-back-button-navigation/tsconfig.json b/apps/angular/55-back-button-navigation/tsconfig.json
new file mode 100644
index 000000000..64d18756d
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/55-back-button-navigation/tsconfig.spec.json b/apps/angular/55-back-button-navigation/tsconfig.spec.json
new file mode 100644
index 000000000..a302b0b88
--- /dev/null
+++ b/apps/angular/55-back-button-navigation/tsconfig.spec.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "module": "preserve",
+ "types": ["jest", "node", "@testing-library/jest-dom"],
+ "moduleResolution": "bundler",
+ "isolatedModules": true
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/apps/angular/57-content-projection-default/.eslintrc.json b/apps/angular/57-content-projection-default/.eslintrc.json
new file mode 100644
index 000000000..8ebcbfd59
--- /dev/null
+++ b/apps/angular/57-content-projection-default/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@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": {}
+ }
+ ]
+}
diff --git a/apps/angular/57-content-projection-default/README.md b/apps/angular/57-content-projection-default/README.md
new file mode 100644
index 000000000..fc4579558
--- /dev/null
+++ b/apps/angular/57-content-projection-default/README.md
@@ -0,0 +1,13 @@
+# Content Projection Default
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-content-projection-default
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/57-content-projection-default/).
diff --git a/apps/angular/57-content-projection-default/project.json b/apps/angular/57-content-projection-default/project.json
new file mode 100644
index 000000000..0630925f4
--- /dev/null
+++ b/apps/angular/57-content-projection-default/project.json
@@ -0,0 +1,82 @@
+{
+ "name": "angular-content-projection-default",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/57-content-projection-default/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:application",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/57-content-projection-default",
+ "index": "apps/angular/57-content-projection-default/src/index.html",
+ "browser": "apps/angular/57-content-projection-default/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/57-content-projection-default/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "apps/angular/57-content-projection-default/public"
+ }
+ ],
+ "styles": [
+ "apps/angular/57-content-projection-default/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-default:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-content-projection-default:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-content-projection-default:build"
+ }
+ },
+ "serve-static": {
+ "executor": "@nx/web:file-server",
+ "options": {
+ "buildTarget": "angular-content-projection-default:build",
+ "staticFilePath": "dist/apps/angular/57-content-projection-default/browser",
+ "spa": true
+ }
+ }
+ }
+}
diff --git a/apps/angular/57-content-projection-default/public/favicon.ico b/apps/angular/57-content-projection-default/public/favicon.ico
new file mode 100644
index 000000000..317ebcb23
Binary files /dev/null and b/apps/angular/57-content-projection-default/public/favicon.ico differ
diff --git a/apps/angular/57-content-projection-default/src/app/app.component.ts b/apps/angular/57-content-projection-default/src/app/app.component.ts
new file mode 100644
index 000000000..b3e370a34
--- /dev/null
+++ b/apps/angular/57-content-projection-default/src/app/app.component.ts
@@ -0,0 +1,16 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { CardComponent } from './card.component';
+
+@Component({
+ imports: [CardComponent],
+ selector: 'app-root',
+ template: `
+
+
+ `,
+ host: {
+ class: 'p-4 block flex flex-col gap-1',
+ },
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AppComponent {}
diff --git a/apps/angular/57-content-projection-default/src/app/app.config.ts b/apps/angular/57-content-projection-default/src/app/app.config.ts
new file mode 100644
index 000000000..034603cfd
--- /dev/null
+++ b/apps/angular/57-content-projection-default/src/app/app.config.ts
@@ -0,0 +1,5 @@
+import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
+
+export const appConfig: ApplicationConfig = {
+ providers: [provideZoneChangeDetection({ eventCoalescing: true })],
+};
diff --git a/apps/angular/57-content-projection-default/src/app/card.component.ts b/apps/angular/57-content-projection-default/src/app/card.component.ts
new file mode 100644
index 000000000..851a6619d
--- /dev/null
+++ b/apps/angular/57-content-projection-default/src/app/card.component.ts
@@ -0,0 +1,22 @@
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
+
+@Component({
+ selector: 'app-card',
+ imports: [],
+ template: `
+ {{ title() }}
+ @if (message()) {
+ {{ message() }}
+ } @else {
+ Aucun message
+ }
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ class: 'p-4 border border-grey rounded-sm flex flex-col w-[200px]',
+ },
+})
+export class CardComponent {
+ title = input.required();
+ message = input(undefined);
+}
diff --git a/apps/angular/57-content-projection-default/src/index.html b/apps/angular/57-content-projection-default/src/index.html
new file mode 100644
index 000000000..8b3015d4b
--- /dev/null
+++ b/apps/angular/57-content-projection-default/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-content-projection-default
+
+
+
+
+
+
+
+
diff --git a/apps/angular/57-content-projection-default/src/main.ts b/apps/angular/57-content-projection-default/src/main.ts
new file mode 100644
index 000000000..f3a7223da
--- /dev/null
+++ b/apps/angular/57-content-projection-default/src/main.ts
@@ -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),
+);
diff --git a/apps/angular/57-content-projection-default/src/styles.scss b/apps/angular/57-content-projection-default/src/styles.scss
new file mode 100644
index 000000000..77e408aa8
--- /dev/null
+++ b/apps/angular/57-content-projection-default/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/angular/57-content-projection-default/tailwind.config.js b/apps/angular/57-content-projection-default/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/angular/57-content-projection-default/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/angular/57-content-projection-default/tsconfig.app.json b/apps/angular/57-content-projection-default/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/57-content-projection-default/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/57-content-projection-default/tsconfig.json b/apps/angular/57-content-projection-default/tsconfig.json
new file mode 100644
index 000000000..32b44a576
--- /dev/null
+++ b/apps/angular/57-content-projection-default/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/58-content-projection-condition/.eslintrc.json b/apps/angular/58-content-projection-condition/.eslintrc.json
new file mode 100644
index 000000000..8ebcbfd59
--- /dev/null
+++ b/apps/angular/58-content-projection-condition/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@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": {}
+ }
+ ]
+}
diff --git a/apps/angular/58-content-projection-condition/README.md b/apps/angular/58-content-projection-condition/README.md
new file mode 100644
index 000000000..755bd8854
--- /dev/null
+++ b/apps/angular/58-content-projection-condition/README.md
@@ -0,0 +1,13 @@
+# Content Projection Condition
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-content-projection-condition
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/58-content-projection-condition/).
diff --git a/apps/angular/58-content-projection-condition/project.json b/apps/angular/58-content-projection-condition/project.json
new file mode 100644
index 000000000..b268984a2
--- /dev/null
+++ b/apps/angular/58-content-projection-condition/project.json
@@ -0,0 +1,85 @@
+{
+ "name": "angular-content-projection-condition",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/angular/58-content-projection-condition/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:application",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/58-content-projection-condition",
+ "index": "apps/angular/58-content-projection-condition/src/index.html",
+ "browser": "apps/angular/58-content-projection-condition/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/angular/58-content-projection-condition/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "apps/angular/58-content-projection-condition/public"
+ }
+ ],
+ "styles": [
+ "apps/angular/58-content-projection-condition/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-condition:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-content-projection-condition:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-content-projection-condition:build"
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint"
+ },
+ "serve-static": {
+ "executor": "@nx/web:file-server",
+ "options": {
+ "buildTarget": "angular-content-projection-condition:build",
+ "staticFilePath": "dist/apps/angular/58-content-projection-condition/browser",
+ "spa": true
+ }
+ }
+ }
+}
diff --git a/apps/angular/58-content-projection-condition/public/favicon.ico b/apps/angular/58-content-projection-condition/public/favicon.ico
new file mode 100644
index 000000000..317ebcb23
Binary files /dev/null and b/apps/angular/58-content-projection-condition/public/favicon.ico differ
diff --git a/apps/angular/58-content-projection-condition/src/app/app.component.ts b/apps/angular/58-content-projection-condition/src/app/app.component.ts
new file mode 100644
index 000000000..afad56f22
--- /dev/null
+++ b/apps/angular/58-content-projection-condition/src/app/app.component.ts
@@ -0,0 +1,22 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { CardComponent } from './card.component';
+
+@Component({
+ imports: [CardComponent],
+ selector: 'app-root',
+ template: `
+
+ Card 1
+ Message 1
+
+
+ Card 2
+ Message 2
+
+ `,
+ host: {
+ class: 'p-4 block flex flex-col gap-1',
+ },
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AppComponent {}
diff --git a/apps/angular/58-content-projection-condition/src/app/app.config.ts b/apps/angular/58-content-projection-condition/src/app/app.config.ts
new file mode 100644
index 000000000..034603cfd
--- /dev/null
+++ b/apps/angular/58-content-projection-condition/src/app/app.config.ts
@@ -0,0 +1,5 @@
+import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
+
+export const appConfig: ApplicationConfig = {
+ providers: [provideZoneChangeDetection({ eventCoalescing: true })],
+};
diff --git a/apps/angular/58-content-projection-condition/src/app/card.component.ts b/apps/angular/58-content-projection-condition/src/app/card.component.ts
new file mode 100644
index 000000000..46925977c
--- /dev/null
+++ b/apps/angular/58-content-projection-condition/src/app/card.component.ts
@@ -0,0 +1,25 @@
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
+
+@Component({
+ selector: 'app-card',
+ template: `
+ @if (small()) {
+
+
+ } @else {
+
+ }
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ class: 'p-4 border border-grey rounded-sm flex flex-col w-[200px]',
+ },
+})
+export class CardComponent {
+ small = input(false);
+}
diff --git a/apps/angular/58-content-projection-condition/src/index.html b/apps/angular/58-content-projection-condition/src/index.html
new file mode 100644
index 000000000..f1768ec71
--- /dev/null
+++ b/apps/angular/58-content-projection-condition/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-content-projection-condition
+
+
+
+
+
+
+
+
diff --git a/apps/angular/58-content-projection-condition/src/main.ts b/apps/angular/58-content-projection-condition/src/main.ts
new file mode 100644
index 000000000..f3a7223da
--- /dev/null
+++ b/apps/angular/58-content-projection-condition/src/main.ts
@@ -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),
+);
diff --git a/apps/angular/58-content-projection-condition/src/styles.scss b/apps/angular/58-content-projection-condition/src/styles.scss
new file mode 100644
index 000000000..77e408aa8
--- /dev/null
+++ b/apps/angular/58-content-projection-condition/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/angular/58-content-projection-condition/tailwind.config.js b/apps/angular/58-content-projection-condition/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/angular/58-content-projection-condition/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/angular/58-content-projection-condition/tsconfig.app.json b/apps/angular/58-content-projection-condition/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/58-content-projection-condition/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/58-content-projection-condition/tsconfig.json b/apps/angular/58-content-projection-condition/tsconfig.json
new file mode 100644
index 000000000..32b44a576
--- /dev/null
+++ b/apps/angular/58-content-projection-condition/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/59-content-projection-defer/.eslintrc.json b/apps/angular/59-content-projection-defer/.eslintrc.json
new file mode 100644
index 000000000..995177b5b
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/.eslintrc.json
@@ -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": {}
+ }
+ ]
+}
diff --git a/apps/angular/59-content-projection-defer/README.md b/apps/angular/59-content-projection-defer/README.md
new file mode 100644
index 000000000..f726842d1
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/README.md
@@ -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/).
diff --git a/apps/angular/59-content-projection-defer/project.json b/apps/angular/59-content-projection-defer/project.json
new file mode 100644
index 000000000..efd270b03
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/project.json
@@ -0,0 +1,83 @@
+{
+ "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",
+ "continuous": true
+ },
+ "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
+ }
+ }
+ }
+}
diff --git a/apps/angular/59-content-projection-defer/public/favicon.ico b/apps/angular/59-content-projection-defer/public/favicon.ico
new file mode 100644
index 000000000..317ebcb23
Binary files /dev/null and b/apps/angular/59-content-projection-defer/public/favicon.ico differ
diff --git a/apps/angular/59-content-projection-defer/src/app/app.component.ts b/apps/angular/59-content-projection-defer/src/app/app.component.ts
new file mode 100644
index 000000000..ae40bc880
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/src/app/app.component.ts
@@ -0,0 +1,23 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { RouterLink, RouterOutlet } from '@angular/router';
+
+@Component({
+ imports: [RouterOutlet, RouterLink],
+ selector: 'app-root',
+ template: `
+
+
+ Page 1
+
+
+ Page 2
+
+
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ class: 'flex flex-col gap-2 ',
+ },
+})
+export class AppComponent {}
diff --git a/apps/angular/59-content-projection-defer/src/app/app.config.ts b/apps/angular/59-content-projection-defer/src/app/app.config.ts
new file mode 100644
index 000000000..faf4d099a
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/src/app/app.config.ts
@@ -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(),
+ ],
+};
diff --git a/apps/angular/59-content-projection-defer/src/app/app.routes.ts b/apps/angular/59-content-projection-defer/src/app/app.routes.ts
new file mode 100644
index 000000000..3ca1b67cc
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/src/app/app.routes.ts
@@ -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' },
+];
diff --git a/apps/angular/59-content-projection-defer/src/app/expandable-card.ts b/apps/angular/59-content-projection-defer/src/app/expandable-card.ts
new file mode 100644
index 000000000..8f446ed80
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/src/app/expandable-card.ts
@@ -0,0 +1,54 @@
+import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
+
+@Component({
+ selector: 'app-expandable-card',
+ template: `
+
+ @if (isExpanded()) {
+
+
+
+ } @else {
+
+
+
+ }
+
+
+
+
+
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ class: 'flex flex-col gap-2 ',
+ },
+})
+export class ExpandableCard {
+ public isExpanded = signal(false);
+}
diff --git a/apps/angular/59-content-projection-defer/src/app/page-1.ts b/apps/angular/59-content-projection-defer/src/app/page-1.ts
new file mode 100644
index 000000000..868d76959
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/src/app/page-1.ts
@@ -0,0 +1,10 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+
+@Component({
+ selector: 'app-page-1',
+ template: `
+ page1
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class Page1 {}
diff --git a/apps/angular/59-content-projection-defer/src/app/page-2.ts b/apps/angular/59-content-projection-defer/src/app/page-2.ts
new file mode 100644
index 000000000..5665466d8
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/src/app/page-2.ts
@@ -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
+
+ Load Post
+
+ @if (postResource.isLoading()) {
+ Loading...
+ } @else if (postResource.status() === ResourceStatus.Error) {
+ Error...
+ } @else {
+ @for (post of postResource.value(); track post.id) {
+
{{ post.title }}
+ }
+ }
+
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [ExpandableCard],
+})
+export class Page2 {
+ public postResource = httpResource(
+ 'https://jsonplaceholder.typicode.com/posts',
+ );
+ protected readonly ResourceStatus = ResourceStatus;
+}
diff --git a/apps/angular/59-content-projection-defer/src/index.html b/apps/angular/59-content-projection-defer/src/index.html
new file mode 100644
index 000000000..79f435fa6
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ angular-content-projection-defer
+
+
+
+
+
+
+
+
diff --git a/apps/angular/59-content-projection-defer/src/main.ts b/apps/angular/59-content-projection-defer/src/main.ts
new file mode 100644
index 000000000..f3a7223da
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/src/main.ts
@@ -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),
+);
diff --git a/apps/angular/59-content-projection-defer/src/styles.scss b/apps/angular/59-content-projection-defer/src/styles.scss
new file mode 100644
index 000000000..77e408aa8
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/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/angular/59-content-projection-defer/tailwind.config.js b/apps/angular/59-content-projection-defer/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/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/angular/59-content-projection-defer/tsconfig.app.json b/apps/angular/59-content-projection-defer/tsconfig.app.json
new file mode 100644
index 000000000..8b5631268
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "moduleResolution": "bundler"
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/angular/59-content-projection-defer/tsconfig.json b/apps/angular/59-content-projection-defer/tsconfig.json
new file mode 100644
index 000000000..32b44a576
--- /dev/null
+++ b/apps/angular/59-content-projection-defer/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "preserve",
+ "moduleResolution": "bundler",
+ "lib": ["dom", "es2022"]
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/apps/angular/6-structural-directive/.eslintrc.json b/apps/angular/6-structural-directive/.eslintrc.json
new file mode 100644
index 000000000..8ebcbfd59
--- /dev/null
+++ b/apps/angular/6-structural-directive/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@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": {}
+ }
+ ]
+}
diff --git a/apps/angular/6-structural-directive/README.md b/apps/angular/6-structural-directive/README.md
new file mode 100644
index 000000000..775b40981
--- /dev/null
+++ b/apps/angular/6-structural-directive/README.md
@@ -0,0 +1,13 @@
+# Structural Directive
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve angular-structural-directive
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/6-permissions/).
diff --git a/apps/angular/6-structural-directive/project.json b/apps/angular/6-structural-directive/project.json
new file mode 100644
index 000000000..399418f90
--- /dev/null
+++ b/apps/angular/6-structural-directive/project.json
@@ -0,0 +1,73 @@
+{
+ "name": "angular-structural-directive",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/angular/6-structural-directive/src",
+ "prefix": "app",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/angular/6-structural-directive",
+ "index": "apps/angular/6-structural-directive/src/index.html",
+ "main": "apps/angular/6-structural-directive/src/main.ts",
+ "polyfills": "apps/angular/6-structural-directive/src/polyfills.ts",
+ "tsConfig": "apps/angular/6-structural-directive/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ "apps/angular/6-structural-directive/src/favicon.ico",
+ "apps/angular/6-structural-directive/src/assets"
+ ],
+ "styles": ["apps/angular/6-structural-directive/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": {
+ "buildTarget": "angular-structural-directive:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-structural-directive:build:development"
+ }
+ },
+ "defaultConfiguration": "development",
+ "continuous": true
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "angular-structural-directive:build"
+ }
+ }
+ }
+}
diff --git a/apps/angular/6-structural-directive/src/app/app.component.ts b/apps/angular/6-structural-directive/src/app/app.component.ts
new file mode 100644
index 000000000..1fcb0c548
--- /dev/null
+++ b/apps/angular/6-structural-directive/src/app/app.component.ts
@@ -0,0 +1,12 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { RouterOutlet } from '@angular/router';
+
+@Component({
+ imports: [RouterOutlet],
+ selector: 'app-root',
+ template: `
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AppComponent {}
diff --git a/apps/angular/6-structural-directive/src/app/app.config.ts b/apps/angular/6-structural-directive/src/app/app.config.ts
new file mode 100644
index 000000000..4b5550da5
--- /dev/null
+++ b/apps/angular/6-structural-directive/src/app/app.config.ts
@@ -0,0 +1,6 @@
+import { ApplicationConfig } from '@angular/core';
+import { provideRouter } from '@angular/router';
+import { APP_ROUTES } from './routes';
+export const appConfig: ApplicationConfig = {
+ providers: [provideRouter(APP_ROUTES)],
+};
diff --git a/apps/permissions/src/app/button.component.ts b/apps/angular/6-structural-directive/src/app/button.component.ts
similarity index 85%
rename from apps/permissions/src/app/button.component.ts
rename to apps/angular/6-structural-directive/src/app/button.component.ts
index b6599e5ef..5d1323605 100644
--- a/apps/permissions/src/app/button.component.ts
+++ b/apps/angular/6-structural-directive/src/app/button.component.ts
@@ -2,9 +2,10 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
- standalone: true,
selector: 'button[app-button]',
- template: ` `,
+ template: `
+
+ `,
host: {
class: 'border border-blue-700 bg-blue-400 p-2 rounded-sm text-white',
},
diff --git a/apps/permissions/src/app/dashboard/admin.component.ts b/apps/angular/6-structural-directive/src/app/dashboard/admin.component.ts
similarity index 95%
rename from apps/permissions/src/app/dashboard/admin.component.ts
rename to apps/angular/6-structural-directive/src/app/dashboard/admin.component.ts
index 72e4b3bef..26bb23284 100644
--- a/apps/permissions/src/app/dashboard/admin.component.ts
+++ b/apps/angular/6-structural-directive/src/app/dashboard/admin.component.ts
@@ -4,7 +4,6 @@ import { ButtonComponent } from '../button.component';
@Component({
selector: 'app-dashboard',
- standalone: true,
imports: [RouterLink, ButtonComponent],
template: `
dashboard for Admin works!
diff --git a/apps/angular/6-structural-directive/src/app/dashboard/manager.component.ts b/apps/angular/6-structural-directive/src/app/dashboard/manager.component.ts
new file mode 100644
index 000000000..60ea7695b
--- /dev/null
+++ b/apps/angular/6-structural-directive/src/app/dashboard/manager.component.ts
@@ -0,0 +1,12 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+
+@Component({
+ selector: 'app-dashboard',
+ imports: [],
+ template: `
+ dashboard for Manager works!
+ Logout
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ManagerDashboardComponent {}
diff --git a/apps/angular/6-structural-directive/src/app/information.component.ts b/apps/angular/6-structural-directive/src/app/information.component.ts
new file mode 100644
index 000000000..ecf937efc
--- /dev/null
+++ b/apps/angular/6-structural-directive/src/app/information.component.ts
@@ -0,0 +1,22 @@
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { UserStore } from './user.store';
+
+@Component({
+ selector: 'app-information',
+ template: `
+ Information Panel
+
+ visible only for super admin
+ visible if manager
+ visible if manager and/or reader
+ visible if manager and/or writer
+ visible if client
+ visible for everyone
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class InformationComponent {
+ private readonly userStore = inject(UserStore);
+
+ user$ = this.userStore.user$;
+}
diff --git a/apps/permissions/src/app/login.component.ts b/apps/angular/6-structural-directive/src/app/login.component.ts
similarity index 80%
rename from apps/permissions/src/app/login.component.ts
rename to apps/angular/6-structural-directive/src/app/login.component.ts
index ab27b11ec..f38e5e5ca 100644
--- a/apps/permissions/src/app/login.component.ts
+++ b/apps/angular/6-structural-directive/src/app/login.component.ts
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { ButtonComponent } from './button.component';
import { InformationComponent } from './information.component';
@@ -14,11 +14,10 @@ import {
import { UserStore } from './user.store';
@Component({
- standalone: true,
imports: [InformationComponent, RouterLink, ButtonComponent],
selector: 'app-login',
template: `
-