diff --git a/README.md b/README.md
index 6a8030dfd..f3dedab3e 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ If you would like to propose a challenge, this project is open source, so feel f
## Challenges
-Check [all 64 challenges](https://angular-challenges.vercel.app/)
+Check [all 65 challenges](https://angular-challenges.vercel.app/)
## Contributors ✨
diff --git a/apps/forms/65-signal-form-edition/README.md b/apps/forms/65-signal-form-edition/README.md
new file mode 100644
index 000000000..228ff0cfb
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/README.md
@@ -0,0 +1,19 @@
+# signal-form-edition
+
+> author: thomas-laforge
+
+### Run Application
+
+```bash
+npx nx serve forms-signal-form-edition
+```
+
+### Run Tests
+
+```bash
+npx nx test forms-signal-form-edition
+```
+
+### Documentation and Instruction
+
+Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/forms/65-signal-form-edition/).
diff --git a/apps/forms/65-signal-form-edition/eslint.config.mjs b/apps/forms/65-signal-form-edition/eslint.config.mjs
new file mode 100644
index 000000000..0e557f6b7
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/eslint.config.mjs
@@ -0,0 +1,34 @@
+import nx from '@nx/eslint-plugin';
+import baseConfig from '../../../eslint.config.mjs';
+
+export default [
+ ...baseConfig,
+ ...nx.configs['flat/angular'],
+ ...nx.configs['flat/angular-template'],
+ {
+ 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',
+ },
+ ],
+ },
+ },
+ {
+ files: ['**/*.html'],
+ // Override or add rules here
+ rules: {},
+ },
+];
diff --git a/apps/forms/65-signal-form-edition/project.json b/apps/forms/65-signal-form-edition/project.json
new file mode 100644
index 000000000..8bbf0f4f2
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/project.json
@@ -0,0 +1,75 @@
+{
+ "name": "forms-signal-form-edition",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/forms/65-signal-form-edition/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular/build:application",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/apps/forms/65-signal-form-edition",
+ "browser": "apps/forms/65-signal-form-edition/src/main.ts",
+ "tsConfig": "apps/forms/65-signal-form-edition/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "apps/forms/65-signal-form-edition/public"
+ }
+ ],
+ "styles": ["apps/forms/65-signal-form-edition/src/styles.scss"]
+ },
+ "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": {
+ "continuous": true,
+ "executor": "@angular/build:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "forms-signal-form-edition:build:production"
+ },
+ "development": {
+ "buildTarget": "forms-signal-form-edition:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint"
+ },
+ "serve-static": {
+ "continuous": true,
+ "executor": "@nx/web:file-server",
+ "options": {
+ "buildTarget": "forms-signal-form-edition:build",
+ "staticFilePath": "dist/apps/forms/65-signal-form-edition/browser",
+ "spa": true
+ }
+ }
+ }
+}
diff --git a/apps/forms/65-signal-form-edition/public/favicon.ico b/apps/forms/65-signal-form-edition/public/favicon.ico
new file mode 100644
index 000000000..317ebcb23
Binary files /dev/null and b/apps/forms/65-signal-form-edition/public/favicon.ico differ
diff --git a/apps/forms/65-signal-form-edition/src/app/app.component.spec.ts b/apps/forms/65-signal-form-edition/src/app/app.component.spec.ts
new file mode 100644
index 000000000..9cdb619d7
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/src/app/app.component.spec.ts
@@ -0,0 +1,96 @@
+import { TestBed } from '@angular/core/testing';
+import { Router } from '@angular/router';
+import { page } from 'vitest/browser';
+import { AppComponent } from './app.component';
+import { appConfig } from './app.config';
+
+describe('AppComponent', () => {
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ providers: appConfig.providers,
+ });
+ const router = TestBed.inject(Router);
+ router.initialNavigation();
+ TestBed.createComponent(AppComponent);
+ });
+
+ describe('When component is rendered', () => {
+ it('Then should display the portal title', async () => {
+ const heading = page.getByRole('heading', {
+ name: /user management portal/i,
+ });
+ await expect.element(heading).toBeInTheDocument();
+ });
+
+ it('Then should display correct information in the user list', async () => {
+ await expect
+ .element(page.getByText('Max Mustermann'))
+ .toBeInTheDocument();
+ await expect.element(page.getByText('John Doe')).toBeInTheDocument();
+ await expect.element(page.getByText('Jane Smith')).toBeInTheDocument();
+ });
+ });
+
+ describe('Given a user wants to add a new user', () => {
+ it('Then should navigate to add form and create user', async () => {
+ const addButton = page.getByRole('button', { name: /add user/i }).first();
+ await addButton.click();
+
+ await expect
+ .element(page.getByRole('heading', { name: /add new user/i }))
+ .toBeInTheDocument();
+
+ await page.getByLabelText(/firstname/i).fill('Antigravity');
+ await page.getByLabelText(/lastname/i).fill('AI');
+ await page.getByLabelText(/age/i).fill('1');
+ await page.getByLabelText(/grade/i).fill('10');
+
+ await page.getByRole('button', { name: /add/i }).click();
+
+ await expect
+ .element(page.getByText('Antigravity AI'))
+ .toBeInTheDocument();
+ });
+ });
+
+ describe('Given a user wants to edit an existing user', () => {
+ it('Then should update the user successfully', async () => {
+ await expect.element(page.getByText('Jane Smith')).toBeInTheDocument();
+
+ const editButtons = await page
+ .getByRole('button', { name: /edit/i })
+ .all();
+ // Jane Smith is the 3rd user in list (id 3)
+ await editButtons[2].click();
+
+ await expect
+ .element(page.getByRole('heading', { name: /edit user/i }))
+ .toBeInTheDocument();
+ await expect
+ .element(page.getByLabelText(/firstname/i))
+ .toHaveValue('Jane');
+
+ await page.getByLabelText(/firstname/i).fill('Janet');
+ await page.getByRole('button', { name: /update/i }).click();
+
+ await expect.element(page.getByText('Janet Smith')).toBeInTheDocument();
+ await expect
+ .element(page.getByText('Jane Smith'))
+ .not.toBeInTheDocument();
+ });
+ });
+
+ describe('Given a user wants to delete a user', () => {
+ it('Then should remove the user from the list', async () => {
+ await expect.element(page.getByText('John Doe')).toBeInTheDocument();
+
+ const deleteButtons = await page
+ .getByRole('button', { name: /delete/i })
+ .all();
+ // John Doe is the 2nd user in list
+ await deleteButtons[1].click();
+
+ await expect.element(page.getByText('John Doe')).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/apps/forms/65-signal-form-edition/src/app/app.component.ts b/apps/forms/65-signal-form-edition/src/app/app.component.ts
new file mode 100644
index 000000000..b1c92a3f1
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/src/app/app.component.ts
@@ -0,0 +1,22 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { RouterOutlet } from '@angular/router';
+
+@Component({
+ imports: [RouterOutlet],
+ selector: 'app-root',
+ template: `
+
+
+
+
+ User Management Portal
+
+
+
+
+
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AppComponent {}
diff --git a/apps/forms/65-signal-form-edition/src/app/app.config.ts b/apps/forms/65-signal-form-edition/src/app/app.config.ts
new file mode 100644
index 000000000..6b0e2826f
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/src/app/app.config.ts
@@ -0,0 +1,23 @@
+import {
+ ApplicationConfig,
+ provideBrowserGlobalErrorListeners,
+} from '@angular/core';
+import { provideRouter, withComponentInputBinding } from '@angular/router';
+import { HomeComponent } from './home.component';
+import { UserFormComponent } from './user-form.component';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideBrowserGlobalErrorListeners(),
+ provideRouter(
+ [
+ { path: '', redirectTo: 'home', pathMatch: 'full' },
+ { path: 'home', component: HomeComponent },
+ { path: 'add', component: UserFormComponent },
+ { path: 'edit/:id', component: UserFormComponent },
+ { path: '**', redirectTo: 'home' },
+ ],
+ withComponentInputBinding(),
+ ),
+ ],
+};
diff --git a/apps/forms/65-signal-form-edition/src/app/fake-backend.service.ts b/apps/forms/65-signal-form-edition/src/app/fake-backend.service.ts
new file mode 100644
index 000000000..13a5b42d0
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/src/app/fake-backend.service.ts
@@ -0,0 +1,44 @@
+import { Injectable } from '@angular/core';
+import { delay, Observable, of } from 'rxjs';
+import { User } from './user.model';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class FakeBackendService {
+ private users: User[] = [
+ { id: 1, firstname: 'Max', lastname: 'Mustermann', age: 30, grade: 10 },
+ { id: 2, firstname: 'John', lastname: 'Doe', age: 25, grade: 8 },
+ { id: 3, firstname: 'Jane', lastname: 'Smith', age: 28, grade: 9 },
+ ];
+
+ getUsers(): Observable {
+ return of([...this.users]).pipe(delay(500));
+ }
+
+ getUser(id: number): Observable {
+ return of(this.users.find((u) => u.id === id)).pipe(delay(500));
+ }
+
+ addUser(user: Omit): Observable {
+ const newUser = {
+ ...user,
+ id: Math.max(...this.users.map((u) => u.id), 0) + 1,
+ };
+ this.users.push(newUser);
+ return of(newUser).pipe(delay(500));
+ }
+
+ updateUser(user: User): Observable {
+ const index = this.users.findIndex((u) => u.id === user.id);
+ if (index !== -1) {
+ this.users[index] = user;
+ }
+ return of(user).pipe(delay(500));
+ }
+
+ deleteUser(id: number): Observable {
+ this.users = this.users.filter((u) => u.id !== id);
+ return of(undefined).pipe(delay(500));
+ }
+}
diff --git a/apps/forms/65-signal-form-edition/src/app/home.component.ts b/apps/forms/65-signal-form-edition/src/app/home.component.ts
new file mode 100644
index 000000000..879da30a5
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/src/app/home.component.ts
@@ -0,0 +1,52 @@
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { rxResource } from '@angular/core/rxjs-interop';
+import { Router } from '@angular/router';
+import { FakeBackendService } from './fake-backend.service';
+import { UserListComponent } from './user-list.component';
+import { User } from './user.model';
+
+@Component({
+ selector: 'app-home',
+ imports: [UserListComponent],
+ template: `
+
+ @if (usersResource.isLoading()) {
+
+ } @else {
+
+ }
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class HomeComponent {
+ private backend = inject(FakeBackendService);
+ private router = inject(Router);
+
+ usersResource = rxResource({
+ stream: () => this.backend.getUsers(),
+ defaultValue: [],
+ });
+
+ onAdd(): void {
+ this.router.navigate(['/add']);
+ }
+
+ onEdit(user: User): void {
+ this.router.navigate(['/edit', user.id]);
+ }
+
+ onDelete(id: number): void {
+ this.backend.deleteUser(id).subscribe(() => {
+ this.usersResource.reload();
+ });
+ }
+}
diff --git a/apps/forms/65-signal-form-edition/src/app/user-form.component.ts b/apps/forms/65-signal-form-edition/src/app/user-form.component.ts
new file mode 100644
index 000000000..e9a6f91cb
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/src/app/user-form.component.ts
@@ -0,0 +1,176 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ effect,
+ inject,
+ input,
+} from '@angular/core';
+import { rxResource } from '@angular/core/rxjs-interop';
+import {
+ FormControl,
+ FormGroup,
+ ReactiveFormsModule,
+ Validators,
+} from '@angular/forms';
+import { Router } from '@angular/router';
+import { of } from 'rxjs';
+import { FakeBackendService } from './fake-backend.service';
+
+@Component({
+ selector: 'app-user-form',
+ imports: [ReactiveFormsModule],
+ template: `
+
+
+ {{ id() ? 'Edit User' : 'Add New User' }}
+
+ @if (id() && userResource.isLoading()) {
+
+ } @else {
+
+ }
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class UserFormComponent {
+ private backend = inject(FakeBackendService);
+ private router = inject(Router);
+
+ id = input();
+
+ userResource = rxResource({
+ params: () => ({ id: this.id() }),
+ stream: ({ params: { id } }) => {
+ return id ? this.backend.getUser(Number(id)) : of(undefined);
+ },
+ defaultValue: undefined,
+ });
+
+ userForm = new FormGroup({
+ firstname: new FormControl('', {
+ nonNullable: true,
+ validators: [Validators.required],
+ }),
+ lastname: new FormControl('', {
+ nonNullable: true,
+ validators: [Validators.required],
+ }),
+ age: new FormControl(0, {
+ nonNullable: true,
+ validators: [Validators.required, Validators.min(0)],
+ }),
+ grade: new FormControl(0, {
+ nonNullable: true,
+ validators: [Validators.required],
+ }),
+ });
+
+ constructor() {
+ effect(() => {
+ const userValue = this.userResource.value();
+ if (userValue) {
+ this.userForm.patchValue(userValue);
+ } else {
+ this.userForm.reset({ firstname: '', lastname: '', age: 0, grade: 0 });
+ }
+ });
+ }
+
+ onSubmit(): void {
+ if (this.userForm.valid) {
+ const userValue = this.userResource.value();
+ const obs = userValue
+ ? this.backend.updateUser({
+ ...this.userForm.getRawValue(),
+ id: userValue.id,
+ })
+ : this.backend.addUser(this.userForm.getRawValue());
+
+ obs.subscribe(() => {
+ this.router.navigate(['/']);
+ });
+ }
+ }
+
+ onCancel(): void {
+ this.router.navigate(['/']);
+ }
+}
diff --git a/apps/forms/65-signal-form-edition/src/app/user-list.component.ts b/apps/forms/65-signal-form-edition/src/app/user-list.component.ts
new file mode 100644
index 000000000..843aaa035
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/src/app/user-list.component.ts
@@ -0,0 +1,84 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ input,
+ output,
+} from '@angular/core';
+import { User } from './user.model';
+
+@Component({
+ selector: 'app-user-list',
+ template: `
+
+
+
Users
+
+
+
+
+
+
+
+ |
+ Name
+ |
+
+ Age
+ |
+
+ Grade
+ |
+
+ Actions
+ |
+
+
+
+ @for (user of users(); track user.id) {
+
+ |
+ {{ user.firstname }} {{ user.lastname }}
+ |
+
+ {{ user.age }}
+ |
+
+ {{ user.grade }}
+ |
+
+
+
+ |
+
+ }
+
+
+
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class UserListComponent {
+ users = input([]);
+ add = output();
+ edit = output();
+ delete = output();
+}
diff --git a/apps/forms/65-signal-form-edition/src/app/user.model.ts b/apps/forms/65-signal-form-edition/src/app/user.model.ts
new file mode 100644
index 000000000..a4e26720a
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/src/app/user.model.ts
@@ -0,0 +1,7 @@
+export interface User {
+ id: number;
+ firstname: string;
+ lastname: string;
+ age: number;
+ grade: number;
+}
diff --git a/apps/forms/65-signal-form-edition/src/index.html b/apps/forms/65-signal-form-edition/src/index.html
new file mode 100644
index 000000000..566dee60e
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ forms-signal-form-edition
+
+
+
+
+
+
+
+
diff --git a/apps/forms/65-signal-form-edition/src/main.ts b/apps/forms/65-signal-form-edition/src/main.ts
new file mode 100644
index 000000000..f3a7223da
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/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/forms/65-signal-form-edition/src/styles.scss b/apps/forms/65-signal-form-edition/src/styles.scss
new file mode 100644
index 000000000..77e408aa8
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/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/forms/65-signal-form-edition/src/test-setup.ts b/apps/forms/65-signal-form-edition/src/test-setup.ts
new file mode 100644
index 000000000..5f7c78e26
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/src/test-setup.ts
@@ -0,0 +1,7 @@
+import '@angular/compiler';
+// prettier-ignore
+import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
+
+setupTestBed({
+ browserMode: true, // Enables visual test preview
+});
diff --git a/apps/forms/65-signal-form-edition/tailwind.config.js b/apps/forms/65-signal-form-edition/tailwind.config.js
new file mode 100644
index 000000000..38183db2c
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/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/forms/65-signal-form-edition/tsconfig.app.json b/apps/forms/65-signal-form-edition/tsconfig.app.json
new file mode 100644
index 000000000..ba6acbecd
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/tsconfig.app.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": []
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
+}
diff --git a/apps/forms/65-signal-form-edition/tsconfig.json b/apps/forms/65-signal-form-edition/tsconfig.json
new file mode 100644
index 000000000..8e74c6bc1
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "isolatedModules": true,
+ "target": "es2022",
+ "moduleResolution": "bundler",
+ "emitDecoratorMetadata": false,
+ "module": "preserve"
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ]
+}
diff --git a/apps/forms/65-signal-form-edition/tsconfig.spec.json b/apps/forms/65-signal-form-edition/tsconfig.spec.json
new file mode 100644
index 000000000..fda356f57
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/tsconfig.spec.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/spec",
+ "target": "es2022",
+ "types": ["vitest/globals", "node"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
+}
diff --git a/apps/forms/65-signal-form-edition/vitest.config.ts b/apps/forms/65-signal-form-edition/vitest.config.ts
new file mode 100644
index 000000000..f0d38dd8c
--- /dev/null
+++ b/apps/forms/65-signal-form-edition/vitest.config.ts
@@ -0,0 +1,23 @@
+import { defineConfig } from 'vite';
+
+import angular from '@analogjs/vite-plugin-angular';
+import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
+import { playwright } from '@vitest/browser-playwright';
+
+export default defineConfig(({ mode }) => ({
+ plugins: [angular(), nxViteTsPaths()],
+ test: {
+ globals: true,
+ setupFiles: ['src/test-setup.ts'],
+ // environment: 'jsdom',
+ include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+ reporters: ['default'],
+ // Vitest browser config
+ browser: {
+ enabled: true,
+ headless: true, // set to true in CI
+ provider: playwright(),
+ instances: [{ browser: 'chromium' }],
+ },
+ },
+}));
diff --git a/challenge-number.json b/challenge-number.json
index 27a1c6ffe..8070415ee 100644
--- a/challenge-number.json
+++ b/challenge-number.json
@@ -1,6 +1,6 @@
{
- "total": 64,
+ "total": 65,
"🟢": 25,
- "🟠": 126,
+ "🟠": 127,
"🔴": 212
}
diff --git a/docs/src/content/docs/challenges/forms/64-form-array.md b/docs/src/content/docs/challenges/forms/64-form-array.md
index e54d01a54..79cd95df8 100644
--- a/docs/src/content/docs/challenges/forms/64-form-array.md
+++ b/docs/src/content/docs/challenges/forms/64-form-array.md
@@ -8,7 +8,6 @@ challengeNumber: 64
command: forms-form-array
sidebar:
order: 126
- badge: New
---
## Information
diff --git a/docs/src/content/docs/challenges/forms/65-signal-form-edition.md b/docs/src/content/docs/challenges/forms/65-signal-form-edition.md
new file mode 100644
index 000000000..f3457e5b7
--- /dev/null
+++ b/docs/src/content/docs/challenges/forms/65-signal-form-edition.md
@@ -0,0 +1,63 @@
+---
+title: 🟠 signal-form-edition
+description: Refactor a user management form to use Angular's new Signal-based Forms API.
+author: thomas-laforge
+contributors:
+ - tomalaforge
+challengeNumber: 65
+command: forms-signal-form-edition
+sidebar:
+ order: 127
+ badge: New
+---
+
+## Information
+
+Angular has introduced a new way to work with forms using Signals. This modern approach provides better reactivity and a more intuitive API compared to traditional Reactive Forms.
+
+In this challenge, you will refactor a user management application. The current implementation uses traditional Reactive Forms. Your goal is to migrate it to the new Signal-based Forms API.
+
+## Statement
+
+The application allows listing, adding, and editing users. It includes:
+
+- **User List**: Displays all users with "Edit" and "Delete" actions.
+- **User Form**: A form used for both adding and editing users.
+- **Fake Backend**: Simulates HTTP calls with a 500ms delay.
+
+Your goal is to **refactor the `UserFormComponent` to use Angular's Signal-based Forms API** while maintaining exactly the same functionality and validation rules.
+
+### Current Implementation
+
+The form currently uses:
+
+- `FormGroup` and `FormControl` for the user fields.
+- `Validators` for mandatory fields (`firstname`, `lastname`, `age`) and minimum age.
+- `patchValue` and `reset` for managing form state during edition.
+
+The application also uses `rxResource` to load users and navigation to handle the editing context.
+
+### Expected Result
+
+After completing the challenge:
+
+- Use Signal-based form instead of `FormControl` and `FormGroup` in `UserFormComponent`.
+- Maintain all existing validation rules and error messages.
+- Correctly handle the transition between "Add" and "Edit" modes.
+- Maintain the same UI and user experience.
+
+## Testing
+
+A comprehensive test suite is provided to ensure your refactoring doesn't break any functionality. You can run the tests using:
+
+```bash
+npx nx test forms-signal-form-edition
+```
+
+These tests verify the entire user management flow, including adding, editing, and deleting users, as well as form validation.
+
+## Constraints
+
+- Do not modify the `FakeBackendService` or `User` model.
+- You can refactor other components if necessary, but the primary focus is the `UserFormComponent`.
+- The form must properly validate inputs before submission.
diff --git a/docs/src/content/docs/es/index.mdx b/docs/src/content/docs/es/index.mdx
index 551515400..557e5a5f2 100644
--- a/docs/src/content/docs/es/index.mdx
+++ b/docs/src/content/docs/es/index.mdx
@@ -13,7 +13,7 @@ hero:
icon: right-arrow
variant: primary
- text: Ir al Desafío más reciente
- link: /es/challenges/forms/64-form-array/
+ link: /es/challenges/forms/65-signal-form-edition/
icon: rocket
- text: Dar una estrella
link: https://github.com/tomalaforge/angular-challenges
@@ -26,8 +26,8 @@ import MyIcon from '../../../components/MyIcon.astro';
import SubscriptionForm from '../../../components/SubscriptionForm.astro';
-
- Este repositorio contiene 64 Desafíos relacionados con Angular, Nx, RxJS, Ngrx y Typescript.
+
+ Este repositorio contiene 65 Desafíos relacionados con Angular, Nx, RxJS, Ngrx y Typescript.
Estos desafíos se resuelven en torno a problemas de la vida real o características específicas para mejorar tus habilidades.
diff --git a/docs/src/content/docs/fr/index.mdx b/docs/src/content/docs/fr/index.mdx
index 89df34b56..42a851ab0 100644
--- a/docs/src/content/docs/fr/index.mdx
+++ b/docs/src/content/docs/fr/index.mdx
@@ -13,7 +13,7 @@ hero:
icon: right-arrow
variant: primary
- text: Aller au dernier Challenge
- link: /fr/challenges/forms/64-form-array/
+ link: /fr/challenges/forms/65-signal-form-edition/
icon: rocket
- text: Donne une étoile
link: https://github.com/tomalaforge/angular-challenges
@@ -26,8 +26,8 @@ import MyIcon from '../../../components/MyIcon.astro';
import SubscriptionForm from '../../../components/SubscriptionForm.astro';
-
- Ce répertoire rassemble 64 Défis liés à Angular, Nx, RxJS, Ngrx et Typescript. Ces défis portent sur des problèmes réels ou des fonctionnalités spécifiques pour améliorer vos compétences.
+
+ Ce répertoire rassemble 65 Défis liés à Angular, Nx, RxJS, Ngrx et Typescript. Ces défis portent sur des problèmes réels ou des fonctionnalités spécifiques pour améliorer vos compétences.
diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx
index cd00076f4..ca68f8ca0 100644
--- a/docs/src/content/docs/index.mdx
+++ b/docs/src/content/docs/index.mdx
@@ -13,7 +13,7 @@ hero:
icon: right-arrow
variant: primary
- text: Go to the latest Challenge
- link: /challenges/forms/64-form-array/
+ link: /challenges/forms/65-signal-form-edition/
icon: rocket
- text: Give a star
link: https://github.com/tomalaforge/angular-challenges
@@ -27,8 +27,8 @@ import MyIcon from '../../components/MyIcon.astro';
import SubscriptionForm from '../../components/SubscriptionForm.astro';
-
- This repository gathers 64 Challenges related to Angular, Nx, RxJS, Ngrx and Typescript.
+
+ This repository gathers 65 Challenges related to Angular, Nx, RxJS, Ngrx and Typescript.
These challenges resolve around real-life issues or specific features to elevate your skills.
diff --git a/docs/src/content/docs/pt/index.mdx b/docs/src/content/docs/pt/index.mdx
index 1fc194d16..6aefb45a3 100644
--- a/docs/src/content/docs/pt/index.mdx
+++ b/docs/src/content/docs/pt/index.mdx
@@ -13,7 +13,7 @@ hero:
icon: right-arrow
variant: primary
- text: Ir para o desafio mais recente
- link: /pt/challenges/forms/64-form-array/
+ link: /pt/challenges/forms/65-signal-form-edition/
icon: rocket
- text: Dar uma estrela
link: https://github.com/tomalaforge/angular-challenges
@@ -26,8 +26,8 @@ import MyIcon from '../../../components/MyIcon.astro';
import SubscriptionForm from '../../../components/SubscriptionForm.astro';
-
- Este repositório possui 64 Desafios relacionados a Angular, Nx, RxJS,
+
+ Este repositório possui 65 Desafios relacionados a Angular, Nx, RxJS,
Ngrx e Typescript.
Esses desafios são voltados para problemas reais ou funcionalidades específicas afim de
melhorar suas habilidades.
diff --git a/docs/src/content/docs/ru/index.mdx b/docs/src/content/docs/ru/index.mdx
index 3e9a36be6..fd77b08af 100644
--- a/docs/src/content/docs/ru/index.mdx
+++ b/docs/src/content/docs/ru/index.mdx
@@ -13,7 +13,7 @@ hero:
icon: right-arrow
variant: primary
- text: Перейти к последней задаче
- link: /ru/challenges/forms/64-form-array/
+ link: /ru/challenges/forms/65-signal-form-edition/
icon: rocket
- text: Добавить звезду
link: https://github.com/tomalaforge/angular-challenges