Skip to content

Commit b137820

Browse files
committed
feat(): create boilerplate for challenge 65
1 parent 1f0e6ee commit b137820

29 files changed

Lines changed: 697 additions & 17 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ If you would like to propose a challenge, this project is open source, so feel f
2929
3030
## Challenges
3131

32-
Check [all 64 challenges](https://angular-challenges.vercel.app/)
32+
Check [all 65 challenges](https://angular-challenges.vercel.app/)
3333

3434
## Contributors ✨
3535

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# signal-form-edition
2+
3+
> author: thomas-laforge
4+
5+
### Run Application
6+
7+
```bash
8+
npx nx serve forms-signal-form-edition
9+
```
10+
11+
### Documentation and Instruction
12+
13+
Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/forms/65-signal-form-edition/).
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import nx from '@nx/eslint-plugin';
2+
import baseConfig from '../../../eslint.config.mjs';
3+
4+
export default [
5+
...baseConfig,
6+
...nx.configs['flat/angular'],
7+
...nx.configs['flat/angular-template'],
8+
{
9+
files: ['**/*.ts'],
10+
rules: {
11+
'@angular-eslint/directive-selector': [
12+
'error',
13+
{
14+
type: 'attribute',
15+
prefix: 'app',
16+
style: 'camelCase',
17+
},
18+
],
19+
'@angular-eslint/component-selector': [
20+
'error',
21+
{
22+
type: 'element',
23+
prefix: 'app',
24+
style: 'kebab-case',
25+
},
26+
],
27+
},
28+
},
29+
{
30+
files: ['**/*.html'],
31+
// Override or add rules here
32+
rules: {},
33+
},
34+
];
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{
2+
"name": "forms-signal-form-edition",
3+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
4+
"projectType": "application",
5+
"prefix": "app",
6+
"sourceRoot": "apps/forms/65-signal-form-edition/src",
7+
"tags": [],
8+
"targets": {
9+
"build": {
10+
"executor": "@angular/build:application",
11+
"outputs": ["{options.outputPath}"],
12+
"options": {
13+
"outputPath": "dist/apps/forms/65-signal-form-edition",
14+
"browser": "apps/forms/65-signal-form-edition/src/main.ts",
15+
"tsConfig": "apps/forms/65-signal-form-edition/tsconfig.app.json",
16+
"inlineStyleLanguage": "scss",
17+
"assets": [
18+
{
19+
"glob": "**/*",
20+
"input": "apps/forms/65-signal-form-edition/public"
21+
}
22+
],
23+
"styles": ["apps/forms/65-signal-form-edition/src/styles.scss"]
24+
},
25+
"configurations": {
26+
"production": {
27+
"budgets": [
28+
{
29+
"type": "initial",
30+
"maximumWarning": "500kb",
31+
"maximumError": "1mb"
32+
},
33+
{
34+
"type": "anyComponentStyle",
35+
"maximumWarning": "4kb",
36+
"maximumError": "8kb"
37+
}
38+
],
39+
"outputHashing": "all"
40+
},
41+
"development": {
42+
"optimization": false,
43+
"extractLicenses": false,
44+
"sourceMap": true
45+
}
46+
},
47+
"defaultConfiguration": "production"
48+
},
49+
"serve": {
50+
"continuous": true,
51+
"executor": "@angular/build:dev-server",
52+
"configurations": {
53+
"production": {
54+
"buildTarget": "forms-signal-form-edition:build:production"
55+
},
56+
"development": {
57+
"buildTarget": "forms-signal-form-edition:build:development"
58+
}
59+
},
60+
"defaultConfiguration": "development"
61+
},
62+
"lint": {
63+
"executor": "@nx/eslint:lint"
64+
},
65+
"serve-static": {
66+
"continuous": true,
67+
"executor": "@nx/web:file-server",
68+
"options": {
69+
"buildTarget": "forms-signal-form-edition:build",
70+
"staticFilePath": "dist/apps/forms/65-signal-form-edition/browser",
71+
"spa": true
72+
}
73+
}
74+
}
75+
}
14.7 KB
Binary file not shown.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { page } from 'vitest/browser';
3+
import { AppComponent } from './app.component';
4+
5+
describe('AppComponent', () => {
6+
beforeEach(async () => {
7+
TestBed.createComponent(AppComponent);
8+
});
9+
10+
test('...', async () => {
11+
const heading = page.getByRole('heading', {
12+
name: /registration form/i,
13+
});
14+
await expect.element(heading).toBeInTheDocument();
15+
});
16+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
import { RouterOutlet } from '@angular/router';
3+
4+
@Component({
5+
imports: [RouterOutlet],
6+
selector: 'app-root',
7+
template: `
8+
<div class="min-h-screen bg-gray-50 px-4 py-8 sm:px-6 lg:px-8">
9+
<div class="mx-auto max-w-4xl">
10+
<header class="mb-8 font-serif">
11+
<h1 class="text-3xl font-bold leading-tight text-gray-900">
12+
User Management Portal
13+
</h1>
14+
</header>
15+
16+
<router-outlet />
17+
</div>
18+
</div>
19+
`,
20+
changeDetection: ChangeDetectionStrategy.OnPush,
21+
})
22+
export class AppComponent {}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {
2+
ApplicationConfig,
3+
provideBrowserGlobalErrorListeners,
4+
} from '@angular/core';
5+
import { provideRouter, withComponentInputBinding } from '@angular/router';
6+
import { HomeComponent } from './home.component';
7+
import { UserFormComponent } from './user-form.component';
8+
9+
export const appConfig: ApplicationConfig = {
10+
providers: [
11+
provideBrowserGlobalErrorListeners(),
12+
provideRouter(
13+
[
14+
{ path: '', redirectTo: 'home', pathMatch: 'full' },
15+
{ path: 'home', component: HomeComponent },
16+
{ path: 'add', component: UserFormComponent },
17+
{ path: 'edit/:id', component: UserFormComponent },
18+
{ path: '**', redirectTo: 'home' },
19+
],
20+
withComponentInputBinding(),
21+
),
22+
],
23+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Injectable } from '@angular/core';
2+
import { delay, Observable, of } from 'rxjs';
3+
import { User } from './user.model';
4+
5+
@Injectable({
6+
providedIn: 'root',
7+
})
8+
export class FakeBackendService {
9+
private users: User[] = [
10+
{ id: 1, firstname: 'Max', lastname: 'Mustermann', age: 30, grade: 10 },
11+
{ id: 2, firstname: 'John', lastname: 'Doe', age: 25, grade: 8 },
12+
{ id: 3, firstname: 'Jane', lastname: 'Smith', age: 28, grade: 9 },
13+
];
14+
15+
getUsers(): Observable<User[]> {
16+
return of([...this.users]).pipe(delay(500));
17+
}
18+
19+
getUser(id: number): Observable<User | undefined> {
20+
return of(this.users.find((u) => u.id === id)).pipe(delay(500));
21+
}
22+
23+
addUser(user: Omit<User, 'id'>): Observable<User> {
24+
const newUser = {
25+
...user,
26+
id: Math.max(...this.users.map((u) => u.id), 0) + 1,
27+
};
28+
this.users.push(newUser);
29+
return of(newUser).pipe(delay(500));
30+
}
31+
32+
updateUser(user: User): Observable<User> {
33+
const index = this.users.findIndex((u) => u.id === user.id);
34+
if (index !== -1) {
35+
this.users[index] = user;
36+
}
37+
return of(user).pipe(delay(500));
38+
}
39+
40+
deleteUser(id: number): Observable<void> {
41+
this.users = this.users.filter((u) => u.id !== id);
42+
return of(undefined).pipe(delay(500));
43+
}
44+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
2+
import { rxResource } from '@angular/core/rxjs-interop';
3+
import { Router } from '@angular/router';
4+
import { FakeBackendService } from './fake-backend.service';
5+
import { UserListComponent } from './user-list.component';
6+
import { User } from './user.model';
7+
8+
@Component({
9+
selector: 'app-home',
10+
imports: [UserListComponent],
11+
template: `
12+
<div class="space-y-6">
13+
@if (usersResource.isLoading()) {
14+
<div
15+
class="flex h-64 items-center justify-center rounded-lg border border-gray-200 bg-white shadow-sm">
16+
<div
17+
class="h-8 w-8 animate-spin rounded-full border-4 border-indigo-500 border-t-transparent"></div>
18+
</div>
19+
} @else {
20+
<app-user-list
21+
[users]="usersResource.value()"
22+
(add)="onAdd()"
23+
(edit)="onEdit($event)"
24+
(delete)="onDelete($event)" />
25+
}
26+
</div>
27+
`,
28+
changeDetection: ChangeDetectionStrategy.OnPush,
29+
})
30+
export class HomeComponent {
31+
private backend = inject(FakeBackendService);
32+
private router = inject(Router);
33+
34+
usersResource = rxResource({
35+
stream: () => this.backend.getUsers(),
36+
defaultValue: [],
37+
});
38+
39+
onAdd(): void {
40+
this.router.navigate(['/add']);
41+
}
42+
43+
onEdit(user: User): void {
44+
this.router.navigate(['/edit', user.id]);
45+
}
46+
47+
onDelete(id: number): void {
48+
this.backend.deleteUser(id).subscribe(() => {
49+
this.usersResource.reload();
50+
});
51+
}
52+
}

0 commit comments

Comments
 (0)