diff --git a/README.md b/README.md
index 4a2ec98..8a971e4 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,7 @@ If you would like to propose a challenge, this project is open source, so feel f
+
@@ -95,7 +96,7 @@ If you would like to propose a challenge, this project is open source, so feel f
diff --git a/apps/bug-cd/README.md b/apps/bug-cd/README.md
index a1cdfbc..f52e1cf 100644
--- a/apps/bug-cd/README.md
+++ b/apps/bug-cd/README.md
@@ -12,14 +12,21 @@ In this small application, we have a navigation menu to route our application to
The goal of the challenge is to debug this application and make it work.
-#### Hint 1
+#### Hints
-If you comment out `routerLinkActive="isSelected"` inside `NavigationComponent`: the application loads correctly.
+
+ Hint 1
+
+ If you comment out `routerLinkActive="isSelected"` inside `NavigationComponent`: the application loads correctly.
+
-#### Hint 2
+
+ Hint 2
If you open the [`RouterLinkActive` source code](https://github.com/angular/angular/blob/main/packages/router/src/directives/router_link_active.ts) and go to **line 196**, Angular is calling `this.cdr.markForCheck` inside a microTask which triggers a new CD cycle. If you comment out this line, the application loads again, however the bug is not inside the Angular Framework. š
šÆ
+
+
### Submitting your work
1. Fork the project
diff --git a/apps/decoupling/.eslintrc.json b/apps/decoupling/.eslintrc.json
new file mode 100644
index 0000000..b428c22
--- /dev/null
+++ b/apps/decoupling/.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/decoupling/README.md b/apps/decoupling/README.md
new file mode 100644
index 0000000..b670716
--- /dev/null
+++ b/apps/decoupling/README.md
@@ -0,0 +1,43 @@
+decoupling component with InjectionToken
+
+> Author: Thomas Laforge
+
+> Big thanks to **Robin Goetz** and his [Spartan Project](https://github.com/goetzrobin/spartan).
+> This challenge was proposed by Robin and is strongly inspired by his project.
+
+### Information
+
+The goal of this challenge is to separate the behavior of a component from its style. For the purpose of this challenge, we will be working on a button element. When we click on it, we will toggle a _disabled_ property which will change the style of the element. This is quite useless in real life but the challenge aims to demonstate a useful concept.
+
+The behavior of the component (referred to as the _brain_ in the Spartan stack) is located in the brain library. The styling part (referred to as the _helmet_) is located inside the helmet library. Both libraries cannot depend on each other because we want to be able to publish them separatly. To help us address the issue, we are using the Nx enforce eslint rule. You can find more details [here](https://nx.dev/core-features/enforce-module-boundaries);
+
+However the button's helmet needs to access the state of the component to style the button differently based on its state. As mention above, we cannot import the `BtnDisabledDirective` directly into the helmet library as done currently. If you go to [`BtnHelmetDirective`](../../libs/decoupling/helmet/src/lib/btn-style.directive.ts), you will encounter a linting error. **A project tagged with "type:hlm" can only depend on libs tagged with "type:core"**.
+
+### Statement
+
+The goal of this challenge is to find a way to decouple both Directives.
+
+### Hint
+
+
+ Hint 1
+ Carefully read the title of the challenge š
+
+
+### Submitting your work
+
+1. Fork the project
+2. clone it
+3. npm ci
+4. `npx nx lint decoupling-helmet` to visualize the error.
+5. `npx nx serve decoupling`
+6. _...work on it_
+7. Commit your work
+8. Submit a PR with a title beginning with **Answer:33** that I will review and other dev can review.
+
+
+
+
+
+
+_You can ask any question on_
diff --git a/apps/decoupling/project.json b/apps/decoupling/project.json
new file mode 100644
index 0000000..6d2c8b7
--- /dev/null
+++ b/apps/decoupling/project.json
@@ -0,0 +1,81 @@
+{
+ "name": "decoupling",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/decoupling/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular-devkit/build-angular:browser",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/decoupling",
+ "index": "apps/decoupling/src/index.html",
+ "main": "apps/decoupling/src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "apps/decoupling/tsconfig.app.json",
+ "assets": [
+ "apps/decoupling/src/favicon.ico",
+ "apps/decoupling/src/assets"
+ ],
+ "styles": ["apps/decoupling/src/styles.scss"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "browserTarget": "decoupling:build:production"
+ },
+ "development": {
+ "browserTarget": "decoupling:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "browserTarget": "decoupling:build"
+ }
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": [
+ "apps/decoupling/**/*.ts",
+ "apps/decoupling/**/*.html"
+ ]
+ }
+ }
+ }
+}
diff --git a/apps/decoupling/src/app/app.component.ts b/apps/decoupling/src/app/app.component.ts
new file mode 100644
index 0000000..e595318
--- /dev/null
+++ b/apps/decoupling/src/app/app.component.ts
@@ -0,0 +1,11 @@
+import { BtnDisabledDirective } from '@angular-challenges/decoupling/brain';
+import { BtnHelmetDirective } from '@angular-challenges/decoupling/helmet';
+import { Component } from '@angular/core';
+
+@Component({
+ standalone: true,
+ imports: [BtnDisabledDirective, BtnHelmetDirective],
+ selector: 'app-root',
+ template: ` Coucou `,
+})
+export class AppComponent {}
diff --git a/apps/decoupling/src/assets/.gitkeep b/apps/decoupling/src/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/apps/decoupling/src/favicon.ico b/apps/decoupling/src/favicon.ico
new file mode 100644
index 0000000..317ebcb
Binary files /dev/null and b/apps/decoupling/src/favicon.ico differ
diff --git a/apps/decoupling/src/index.html b/apps/decoupling/src/index.html
new file mode 100644
index 0000000..c1bf2b7
--- /dev/null
+++ b/apps/decoupling/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ decoupling
+
+
+
+
+
+
+
+
diff --git a/apps/decoupling/src/main.ts b/apps/decoupling/src/main.ts
new file mode 100644
index 0000000..31c5da4
--- /dev/null
+++ b/apps/decoupling/src/main.ts
@@ -0,0 +1,4 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent).catch((err) => console.error(err));
diff --git a/apps/decoupling/src/styles.scss b/apps/decoupling/src/styles.scss
new file mode 100644
index 0000000..77e408a
--- /dev/null
+++ b/apps/decoupling/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/decoupling/tailwind.config.js b/apps/decoupling/tailwind.config.js
new file mode 100644
index 0000000..38183db
--- /dev/null
+++ b/apps/decoupling/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/decoupling/tsconfig.app.json b/apps/decoupling/tsconfig.app.json
new file mode 100644
index 0000000..fff4a41
--- /dev/null
+++ b/apps/decoupling/tsconfig.app.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": []
+ },
+ "files": ["src/main.ts"],
+ "include": ["src/**/*.d.ts"],
+ "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
+}
diff --git a/apps/decoupling/tsconfig.editor.json b/apps/decoupling/tsconfig.editor.json
new file mode 100644
index 0000000..78ac03d
--- /dev/null
+++ b/apps/decoupling/tsconfig.editor.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "include": [
+ "src/**/*.ts",
+ "../../libs/decoupling/brain/src/lib/button-disabled.directive.ts",
+ "../../libs/decoupling/helmet/src/lib/btn-style.directive.ts"
+ ],
+ "compilerOptions": {
+ "types": []
+ }
+}
diff --git a/apps/decoupling/tsconfig.json b/apps/decoupling/tsconfig.json
new file mode 100644
index 0000000..0731542
--- /dev/null
+++ b/apps/decoupling/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.editor.json"
+ }
+ ],
+ "extends": "../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/libs/decoupling/.eslintrc.json b/libs/decoupling/.eslintrc.json
new file mode 100644
index 0000000..d45fd0d
--- /dev/null
+++ b/libs/decoupling/.eslintrc.json
@@ -0,0 +1,61 @@
+{
+ "root": true,
+ "ignorePatterns": ["**/*"],
+ "plugins": ["@nx"],
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+ "rules": {
+ "@nx/enforce-module-boundaries": [
+ "error",
+ {
+ "enforceBuildableLibDependency": true,
+ "allow": [],
+ "depConstraints": [
+ {
+ "sourceTag": "*",
+ "onlyDependOnLibsWithTags": ["*"]
+ },
+ {
+ "sourceTag": "type:hlm",
+ "onlyDependOnLibsWithTags": ["type:core"]
+ },
+ {
+ "sourceTag": "type:brain",
+ "onlyDependOnLibsWithTags": ["type:core"]
+ }
+ ]
+ }
+ ],
+ "@angular-eslint/no-host-metadata-property": [
+ "error",
+ {
+ "allowStatic": true
+ }
+ ]
+ }
+ },
+ {
+ "files": ["*.ts", "*.tsx"],
+ "extends": ["plugin:@nx/typescript"],
+ "rules": {}
+ },
+ {
+ "files": ["*.js", "*.jsx"],
+ "extends": ["plugin:@nx/javascript"],
+ "rules": {}
+ },
+ {
+ "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"],
+ "env": {
+ "jest": true
+ },
+ "rules": {}
+ },
+ {
+ "files": "*.json",
+ "parser": "jsonc-eslint-parser",
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/decoupling/brain/.eslintrc.json b/libs/decoupling/brain/.eslintrc.json
new file mode 100644
index 0000000..daf4b3f
--- /dev/null
+++ b/libs/decoupling/brain/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "lib",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "lib",
+ "style": "kebab-case"
+ }
+ ]
+ },
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/decoupling/brain/README.md b/libs/decoupling/brain/README.md
new file mode 100644
index 0000000..f415873
--- /dev/null
+++ b/libs/decoupling/brain/README.md
@@ -0,0 +1,7 @@
+# decoupling-brain
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test decoupling-brain` to execute the unit tests.
diff --git a/libs/decoupling/brain/jest.config.ts b/libs/decoupling/brain/jest.config.ts
new file mode 100644
index 0000000..443cc73
--- /dev/null
+++ b/libs/decoupling/brain/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+export default {
+ displayName: 'decoupling-brain',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../coverage/libs/decoupling/brain',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/libs/decoupling/brain/ng-package.json b/libs/decoupling/brain/ng-package.json
new file mode 100644
index 0000000..5078abb
--- /dev/null
+++ b/libs/decoupling/brain/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
+ "dest": "../../../dist/libs/decoupling/brain",
+ "lib": {
+ "entryFile": "src/index.ts"
+ }
+}
diff --git a/libs/decoupling/brain/package.json b/libs/decoupling/brain/package.json
new file mode 100644
index 0000000..710c0e4
--- /dev/null
+++ b/libs/decoupling/brain/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@angular-challenges/decoupling/brain",
+ "version": "0.0.1",
+ "peerDependencies": {
+ "@angular/common": "^16.1.0",
+ "@angular/core": "^16.1.0"
+ },
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "sideEffects": false
+}
diff --git a/libs/decoupling/brain/project.json b/libs/decoupling/brain/project.json
new file mode 100644
index 0000000..f9a1b87
--- /dev/null
+++ b/libs/decoupling/brain/project.json
@@ -0,0 +1,50 @@
+{
+ "name": "decoupling-brain",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/decoupling/brain/src",
+ "prefix": "lib",
+ "tags": ["type:brain"],
+ "projectType": "library",
+ "targets": {
+ "build": {
+ "executor": "@nx/angular:ng-packagr-lite",
+ "outputs": ["{workspaceRoot}/dist/{projectRoot}"],
+ "options": {
+ "project": "libs/decoupling/brain/ng-package.json"
+ },
+ "configurations": {
+ "production": {
+ "tsConfig": "libs/decoupling/brain/tsconfig.lib.prod.json"
+ },
+ "development": {
+ "tsConfig": "libs/decoupling/brain/tsconfig.lib.json"
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "libs/decoupling/brain/jest.config.ts",
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "codeCoverage": true
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": [
+ "libs/decoupling/brain/**/*.ts",
+ "libs/decoupling/brain/**/*.html"
+ ]
+ }
+ }
+ }
+}
diff --git a/libs/decoupling/brain/src/index.ts b/libs/decoupling/brain/src/index.ts
new file mode 100644
index 0000000..2bf3f9a
--- /dev/null
+++ b/libs/decoupling/brain/src/index.ts
@@ -0,0 +1,4 @@
+export {
+ BtnDisabledDirective,
+ ButtonState,
+} from './lib/button-disabled.directive';
diff --git a/libs/decoupling/brain/src/lib/button-disabled.directive.ts b/libs/decoupling/brain/src/lib/button-disabled.directive.ts
new file mode 100644
index 0000000..e7a7f45
--- /dev/null
+++ b/libs/decoupling/brain/src/lib/button-disabled.directive.ts
@@ -0,0 +1,20 @@
+/* eslint-disable @angular-eslint/directive-selector */
+/* eslint-disable @angular-eslint/no-host-metadata-property */
+import { Directive, WritableSignal, signal } from '@angular/core';
+
+export type ButtonState = 'enabled' | 'disabled';
+
+@Directive({
+ selector: 'button[btnDisabled]',
+ standalone: true,
+ host: {
+ '(click)': 'toggleState()',
+ },
+})
+export class BtnDisabledDirective {
+ state: WritableSignal = signal('enabled');
+
+ toggleState() {
+ this.state.set(this.state() === 'enabled' ? 'disabled' : 'enabled');
+ }
+}
diff --git a/libs/decoupling/brain/src/test-setup.ts b/libs/decoupling/brain/src/test-setup.ts
new file mode 100644
index 0000000..ab1eeeb
--- /dev/null
+++ b/libs/decoupling/brain/src/test-setup.ts
@@ -0,0 +1,8 @@
+// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment
+globalThis.ngJest = {
+ testEnvironmentOptions: {
+ errorOnUnknownElements: true,
+ errorOnUnknownProperties: true,
+ },
+};
+import 'jest-preset-angular/setup-jest';
diff --git a/libs/decoupling/brain/tsconfig.json b/libs/decoupling/brain/tsconfig.json
new file mode 100644
index 0000000..5cf0a16
--- /dev/null
+++ b/libs/decoupling/brain/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/libs/decoupling/brain/tsconfig.lib.json b/libs/decoupling/brain/tsconfig.lib.json
new file mode 100644
index 0000000..9b49be7
--- /dev/null
+++ b/libs/decoupling/brain/tsconfig.lib.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "src/**/*.spec.ts",
+ "src/test-setup.ts",
+ "jest.config.ts",
+ "src/**/*.test.ts"
+ ],
+ "include": ["src/**/*.ts"]
+}
diff --git a/libs/decoupling/brain/tsconfig.lib.prod.json b/libs/decoupling/brain/tsconfig.lib.prod.json
new file mode 100644
index 0000000..61b5237
--- /dev/null
+++ b/libs/decoupling/brain/tsconfig.lib.prod.json
@@ -0,0 +1,7 @@
+{
+ "extends": "./tsconfig.lib.json",
+ "compilerOptions": {
+ "declarationMap": false
+ },
+ "angularCompilerOptions": {}
+}
diff --git a/libs/decoupling/brain/tsconfig.spec.json b/libs/decoupling/brain/tsconfig.spec.json
new file mode 100644
index 0000000..f858ef7
--- /dev/null
+++ b/libs/decoupling/brain/tsconfig.spec.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "module": "commonjs",
+ "target": "es2016",
+ "types": ["jest", "node"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/libs/decoupling/core/.eslintrc.json b/libs/decoupling/core/.eslintrc.json
new file mode 100644
index 0000000..daf4b3f
--- /dev/null
+++ b/libs/decoupling/core/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "lib",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "lib",
+ "style": "kebab-case"
+ }
+ ]
+ },
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/decoupling/core/README.md b/libs/decoupling/core/README.md
new file mode 100644
index 0000000..485484a
--- /dev/null
+++ b/libs/decoupling/core/README.md
@@ -0,0 +1,7 @@
+# decoupling-core
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test decoupling-core` to execute the unit tests.
diff --git a/libs/decoupling/core/jest.config.ts b/libs/decoupling/core/jest.config.ts
new file mode 100644
index 0000000..f755dfa
--- /dev/null
+++ b/libs/decoupling/core/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+export default {
+ displayName: 'decoupling-core',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../coverage/libs/decoupling/core',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/libs/decoupling/core/ng-package.json b/libs/decoupling/core/ng-package.json
new file mode 100644
index 0000000..9a2d663
--- /dev/null
+++ b/libs/decoupling/core/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
+ "dest": "../../../dist/libs/decoupling/core",
+ "lib": {
+ "entryFile": "src/index.ts"
+ }
+}
diff --git a/libs/decoupling/core/package.json b/libs/decoupling/core/package.json
new file mode 100644
index 0000000..52fc140
--- /dev/null
+++ b/libs/decoupling/core/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@angular-challenges/decoupling/core",
+ "version": "0.0.1",
+ "peerDependencies": {
+ "@angular/common": "^16.1.0",
+ "@angular/core": "^16.1.0"
+ },
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "sideEffects": false
+}
diff --git a/libs/decoupling/core/project.json b/libs/decoupling/core/project.json
new file mode 100644
index 0000000..68cc8e2
--- /dev/null
+++ b/libs/decoupling/core/project.json
@@ -0,0 +1,50 @@
+{
+ "name": "decoupling-core",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/decoupling/core/src",
+ "prefix": "lib",
+ "tags": ["type:core"],
+ "projectType": "library",
+ "targets": {
+ "build": {
+ "executor": "@nx/angular:ng-packagr-lite",
+ "outputs": ["{workspaceRoot}/dist/{projectRoot}"],
+ "options": {
+ "project": "libs/decoupling/core/ng-package.json"
+ },
+ "configurations": {
+ "production": {
+ "tsConfig": "libs/decoupling/core/tsconfig.lib.prod.json"
+ },
+ "development": {
+ "tsConfig": "libs/decoupling/core/tsconfig.lib.json"
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "libs/decoupling/core/jest.config.ts",
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "codeCoverage": true
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": [
+ "libs/decoupling/core/**/*.ts",
+ "libs/decoupling/core/**/*.html"
+ ]
+ }
+ }
+ }
+}
diff --git a/libs/decoupling/core/src/index.ts b/libs/decoupling/core/src/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/libs/decoupling/core/src/test-setup.ts b/libs/decoupling/core/src/test-setup.ts
new file mode 100644
index 0000000..ab1eeeb
--- /dev/null
+++ b/libs/decoupling/core/src/test-setup.ts
@@ -0,0 +1,8 @@
+// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment
+globalThis.ngJest = {
+ testEnvironmentOptions: {
+ errorOnUnknownElements: true,
+ errorOnUnknownProperties: true,
+ },
+};
+import 'jest-preset-angular/setup-jest';
diff --git a/libs/decoupling/core/tsconfig.json b/libs/decoupling/core/tsconfig.json
new file mode 100644
index 0000000..5cf0a16
--- /dev/null
+++ b/libs/decoupling/core/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/libs/decoupling/core/tsconfig.lib.json b/libs/decoupling/core/tsconfig.lib.json
new file mode 100644
index 0000000..9b49be7
--- /dev/null
+++ b/libs/decoupling/core/tsconfig.lib.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "src/**/*.spec.ts",
+ "src/test-setup.ts",
+ "jest.config.ts",
+ "src/**/*.test.ts"
+ ],
+ "include": ["src/**/*.ts"]
+}
diff --git a/libs/decoupling/core/tsconfig.lib.prod.json b/libs/decoupling/core/tsconfig.lib.prod.json
new file mode 100644
index 0000000..61b5237
--- /dev/null
+++ b/libs/decoupling/core/tsconfig.lib.prod.json
@@ -0,0 +1,7 @@
+{
+ "extends": "./tsconfig.lib.json",
+ "compilerOptions": {
+ "declarationMap": false
+ },
+ "angularCompilerOptions": {}
+}
diff --git a/libs/decoupling/core/tsconfig.spec.json b/libs/decoupling/core/tsconfig.spec.json
new file mode 100644
index 0000000..f858ef7
--- /dev/null
+++ b/libs/decoupling/core/tsconfig.spec.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "module": "commonjs",
+ "target": "es2016",
+ "types": ["jest", "node"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/libs/decoupling/helmet/.eslintrc.json b/libs/decoupling/helmet/.eslintrc.json
new file mode 100644
index 0000000..daf4b3f
--- /dev/null
+++ b/libs/decoupling/helmet/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "lib",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "lib",
+ "style": "kebab-case"
+ }
+ ]
+ },
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/decoupling/helmet/README.md b/libs/decoupling/helmet/README.md
new file mode 100644
index 0000000..b2ae715
--- /dev/null
+++ b/libs/decoupling/helmet/README.md
@@ -0,0 +1,7 @@
+# decoupling-helmet
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test decoupling-helmet` to execute the unit tests.
diff --git a/libs/decoupling/helmet/jest.config.ts b/libs/decoupling/helmet/jest.config.ts
new file mode 100644
index 0000000..961b9e9
--- /dev/null
+++ b/libs/decoupling/helmet/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+export default {
+ displayName: 'decoupling-helmet',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../coverage/libs/decoupling/helmet',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/libs/decoupling/helmet/ng-package.json b/libs/decoupling/helmet/ng-package.json
new file mode 100644
index 0000000..9bfd33a
--- /dev/null
+++ b/libs/decoupling/helmet/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
+ "dest": "../../../dist/libs/decoupling/helmet",
+ "lib": {
+ "entryFile": "src/index.ts"
+ }
+}
diff --git a/libs/decoupling/helmet/package.json b/libs/decoupling/helmet/package.json
new file mode 100644
index 0000000..fa6f573
--- /dev/null
+++ b/libs/decoupling/helmet/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@angular-challenges/decoupling/helmet",
+ "version": "0.0.1",
+ "peerDependencies": {
+ "@angular/common": "^16.1.0",
+ "@angular/core": "^16.1.0"
+ },
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "sideEffects": false
+}
diff --git a/libs/decoupling/helmet/project.json b/libs/decoupling/helmet/project.json
new file mode 100644
index 0000000..e9f1367
--- /dev/null
+++ b/libs/decoupling/helmet/project.json
@@ -0,0 +1,50 @@
+{
+ "name": "decoupling-helmet",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/decoupling/helmet/src",
+ "prefix": "lib",
+ "tags": ["type:hlm"],
+ "projectType": "library",
+ "targets": {
+ "build": {
+ "executor": "@nx/angular:ng-packagr-lite",
+ "outputs": ["{workspaceRoot}/dist/{projectRoot}"],
+ "options": {
+ "project": "libs/decoupling/helmet/ng-package.json"
+ },
+ "configurations": {
+ "production": {
+ "tsConfig": "libs/decoupling/helmet/tsconfig.lib.prod.json"
+ },
+ "development": {
+ "tsConfig": "libs/decoupling/helmet/tsconfig.lib.json"
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "libs/decoupling/helmet/jest.config.ts",
+ "passWithNoTests": true
+ },
+ "configurations": {
+ "ci": {
+ "ci": true,
+ "codeCoverage": true
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": [
+ "libs/decoupling/helmet/**/*.ts",
+ "libs/decoupling/helmet/**/*.html"
+ ]
+ }
+ }
+ }
+}
diff --git a/libs/decoupling/helmet/src/index.ts b/libs/decoupling/helmet/src/index.ts
new file mode 100644
index 0000000..4909993
--- /dev/null
+++ b/libs/decoupling/helmet/src/index.ts
@@ -0,0 +1 @@
+export { BtnHelmetDirective } from './lib/btn-style.directive';
diff --git a/libs/decoupling/helmet/src/lib/btn-style.directive.ts b/libs/decoupling/helmet/src/lib/btn-style.directive.ts
new file mode 100644
index 0000000..b8ff764
--- /dev/null
+++ b/libs/decoupling/helmet/src/lib/btn-style.directive.ts
@@ -0,0 +1,33 @@
+/* eslint-disable @angular-eslint/directive-selector */
+import { BtnDisabledDirective } from '@angular-challenges/decoupling/brain';
+import {
+ Directive,
+ ElementRef,
+ Renderer2,
+ effect,
+ inject,
+ signal,
+} from '@angular/core';
+
+@Directive({
+ selector: 'button[hlm]',
+ standalone: true,
+ host: {
+ class:
+ 'border border-black p-4 rounded-md bg-white data-[state=disabled]:bg-gray-400 data-[state=disabled]:text-white',
+ },
+})
+export class BtnHelmetDirective {
+ btnState = inject(BtnDisabledDirective, { self: true });
+ public state = this.btnState?.state ?? signal('disabled').asReadonly();
+ private renderer = inject(Renderer2);
+ private element = inject(ElementRef);
+
+ private rendererEffect = effect(() => {
+ this.renderer.setAttribute(
+ this.element.nativeElement,
+ 'data-state',
+ this.state()
+ );
+ });
+}
diff --git a/libs/decoupling/helmet/src/test-setup.ts b/libs/decoupling/helmet/src/test-setup.ts
new file mode 100644
index 0000000..ab1eeeb
--- /dev/null
+++ b/libs/decoupling/helmet/src/test-setup.ts
@@ -0,0 +1,8 @@
+// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment
+globalThis.ngJest = {
+ testEnvironmentOptions: {
+ errorOnUnknownElements: true,
+ errorOnUnknownProperties: true,
+ },
+};
+import 'jest-preset-angular/setup-jest';
diff --git a/libs/decoupling/helmet/tsconfig.json b/libs/decoupling/helmet/tsconfig.json
new file mode 100644
index 0000000..5cf0a16
--- /dev/null
+++ b/libs/decoupling/helmet/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/libs/decoupling/helmet/tsconfig.lib.json b/libs/decoupling/helmet/tsconfig.lib.json
new file mode 100644
index 0000000..9b49be7
--- /dev/null
+++ b/libs/decoupling/helmet/tsconfig.lib.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "src/**/*.spec.ts",
+ "src/test-setup.ts",
+ "jest.config.ts",
+ "src/**/*.test.ts"
+ ],
+ "include": ["src/**/*.ts"]
+}
diff --git a/libs/decoupling/helmet/tsconfig.lib.prod.json b/libs/decoupling/helmet/tsconfig.lib.prod.json
new file mode 100644
index 0000000..61b5237
--- /dev/null
+++ b/libs/decoupling/helmet/tsconfig.lib.prod.json
@@ -0,0 +1,7 @@
+{
+ "extends": "./tsconfig.lib.json",
+ "compilerOptions": {
+ "declarationMap": false
+ },
+ "angularCompilerOptions": {}
+}
diff --git a/libs/decoupling/helmet/tsconfig.spec.json b/libs/decoupling/helmet/tsconfig.spec.json
new file mode 100644
index 0000000..f858ef7
--- /dev/null
+++ b/libs/decoupling/helmet/tsconfig.spec.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "module": "commonjs",
+ "target": "es2016",
+ "types": ["jest", "node"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 81073fa..f432fe9 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -17,6 +17,15 @@
"paths": {
"@angular-challenges/cli": ["libs/cli/src/index.ts"],
"@angular-challenges/custom-plugin": ["libs/custom-plugin/src/index.ts"],
+ "@angular-challenges/decoupling/brain": [
+ "libs/decoupling/brain/src/index.ts"
+ ],
+ "@angular-challenges/decoupling/core": [
+ "libs/decoupling/core/src/index.ts"
+ ],
+ "@angular-challenges/decoupling/helmet": [
+ "libs/decoupling/helmet/src/index.ts"
+ ],
"@angular-challenges/fake-utils": ["libs/fake-utils/src/index.ts"],
"@angular-challenges/module-to-standalone/admin/feature": [
"libs/module-to-standalone/admin/feature/src/index.ts"