Skip to content

Commit d03331f

Browse files
committed
feat: implement todo backend mock service and update api interceptor
- added TodoBackendMockService to handle CRUD operations for todos - modified apiInterceptor to route requests to the mock service - updated BasicComponent to utilize the new restResource API - improved loading indicators in the template using Angular signals
1 parent e5e1dd0 commit d03331f

5 files changed

Lines changed: 158 additions & 27 deletions

File tree

.github/copilot-instructions.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Project Guidelines
2+
3+
Follow modern Angular syntax and best practices like
4+
5+
* use zone-less change detection and OnPush components
6+
* when changing something, always perform immutable update and use immutable array APIs
7+
* make sure to keep components as lean as possible and move any real logic to services
8+
* always use new Angular control flow with @if, @else, @for, @switch, @case, @let and never use `ngIf`, `ngFor`, `ngSwitch`, and directives where new Angular control flow can be used
9+
* use signals to consume data in template exclusively, this means there won't be any `async` pipes in the template
10+
* use Angular signals and signal-based APIs, eg `viewChild` instead of `@ViewChild` or `output` instead of `@Output`
11+
* use Angular schematics using `ng generate` to generate new files, that way you will follow prescribed conventions from angular.json
12+
* always escape characters like `@` in the template with `@` because this is now Angular reserved character

.github/git-commit-instructions.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use conventional commit, feat, fix, chore, test, docs, refactor, perf, style, ci, build, revert
2+
3+
main point in the main message, keep it short, the code changes always have precedence over other changes like style or documentation,
4+
the main commit message has to always be about the changes to the code wich affects real functionality, NOT docs, or AI guidelines, or other non-code changes
5+
6+
if meaningful, highlight the most impactful changes in the commit body or explain why the change was made, if not obvious
7+
other changes like style, guidelines, refactors can be in the commit body
8+
9+
always use lowercase for everything besides abbreviation like API, UI, or names like Angular, RxJS, etc.
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
1+
import { inject, isDevMode } from '@angular/core';
12
import { HttpInterceptorFn } from '@angular/common/http';
3+
import { delay } from 'rxjs';
4+
5+
import { TodoBackendMockService } from '../mock/todo-backend-mock.service';
26

37
export const apiInterceptor: HttpInterceptorFn = (req, next) => {
8+
const todoBackendMock = inject(TodoBackendMockService);
49
if (req.url.includes('assets')) {
510
return next(req);
611
} else {
712
req = req.clone({
813
url: `https://681db05cf74de1d219b09e9c.mockapi.io/api/v1/` + req.url,
914
});
10-
return next(req);
15+
if (false) {
16+
// hit real server in dev mode
17+
return next(req);
18+
} else {
19+
// hit mock todo server in prod mode so every visitor gets their own todo data
20+
return todoBackendMock.handleRequest(req).pipe(delay(250));
21+
}
1122
}
1223
};
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { Injectable } from '@angular/core';
2+
import { HttpRequest, HttpResponse } from '@angular/common/http';
3+
import { Observable, of } from 'rxjs';
4+
5+
import { Todo } from '../../model/todo.model';
6+
7+
@Injectable({
8+
providedIn: 'root',
9+
})
10+
export class TodoBackendMockService {
11+
todos: Todo[] = [
12+
{
13+
id: '7073bc48-5372-40f0-8d76-99c54c5cd7bd',
14+
description: 'Learn Angular',
15+
completed: true,
16+
createdAt: new Date().toISOString(),
17+
},
18+
{
19+
id: 'b1c2d3e4-f5a6-7b8c-9d0e-f1a2b3c4d5e6',
20+
description: 'Understand Angular Signals',
21+
completed: true,
22+
createdAt: new Date().toISOString(),
23+
},
24+
{
25+
id: 'f7e8d9c0-b1a2-3c4d-5e6f-7g8h9i0j1k2l',
26+
description: 'Lern about Angular resource',
27+
completed: false,
28+
createdAt: new Date().toISOString(),
29+
},
30+
{
31+
id: '12345678-90ab-cdef-ghij-klmnopqrstuv',
32+
description: 'Use restResource',
33+
completed: false,
34+
createdAt: new Date().toISOString(),
35+
}
36+
];
37+
38+
// a method which handles http requests and performs crud on the resource based on which HTTP method is used and payload
39+
handleRequest(request: HttpRequest<unknown>): Observable<HttpResponse<any>> {
40+
const { method, url, body } = request;
41+
const urlParts = url.split('/');
42+
const id = urlParts[urlParts.length - 1];
43+
44+
// Helper to ensure immutability
45+
const clone = <T>(data: T): T => JSON.parse(JSON.stringify(data));
46+
47+
// GET /todos
48+
if (method === 'GET' && url.endsWith('/todos')) {
49+
return of(new HttpResponse({ status: 200, body: clone(this.todos) }));
50+
}
51+
52+
// GET /todos/:id
53+
if (method === 'GET' && url.includes('/todos/')) {
54+
const todo = this.todos.find(t => t.id === id);
55+
if (todo) {
56+
return of(new HttpResponse({ status: 200, body: clone(todo) }));
57+
}
58+
return of(new HttpResponse({ status: 404, body: { message: 'Todo not found' } }));
59+
}
60+
61+
// POST /todos
62+
if (method === 'POST' && url.endsWith('/todos')) {
63+
const newTodo = body as Partial<Todo>;
64+
if (!newTodo.description) {
65+
return of(new HttpResponse({ status: 400, body: { message: 'Description is required' } }));
66+
}
67+
const todo: Todo = {
68+
id: crypto.randomUUID(),
69+
description: newTodo.description,
70+
completed: newTodo.completed || false,
71+
createdAt: new Date().toISOString(),
72+
};
73+
this.todos = [...this.todos, todo];
74+
return of(new HttpResponse({ status: 201, body: clone(todo) }));
75+
}
76+
77+
// PUT /todos/:id
78+
if (method === 'PUT' && url.includes('/todos/')) {
79+
const index = this.todos.findIndex(t => t.id === id);
80+
if (index > -1) {
81+
const updatedTodo = { ...this.todos[index], ...(body as Partial<Todo>) };
82+
this.todos = [
83+
...this.todos.slice(0, index),
84+
updatedTodo,
85+
...this.todos.slice(index + 1),
86+
];
87+
return of(new HttpResponse({ status: 200, body: clone(updatedTodo) }));
88+
}
89+
return of(new HttpResponse({ status: 404, body: { message: 'Todo not found' } }));
90+
}
91+
92+
// DELETE /todos/:id
93+
if (method === 'DELETE' && url.includes('/todos/')) {
94+
const index = this.todos.findIndex(t => t.id === id);
95+
if (index > -1) {
96+
this.todos = [
97+
...this.todos.slice(0, index),
98+
...this.todos.slice(index + 1),
99+
];
100+
return of(new HttpResponse({ status: 204 })); // No Content
101+
}
102+
return of(new HttpResponse({ status: 404, body: { message: 'Todo not found' } }));
103+
}
104+
105+
// Fallback for unhandled routes/methods
106+
return of(new HttpResponse({ status: 404, body: { message: 'Not Found' } }));
107+
}
108+
}

projects/showcase/src/app/feature/example/basic/basic.component.ts

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; // Removed inject
2-
1+
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
32
import { MatFormField, MatInput } from '@angular/material/input';
43
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
54

6-
import { restResource } from '@angular-experts/resource'; // Added back
5+
import { restResource } from '@angular-experts/resource';
6+
77
import { Todo } from '../../../model/todo.model';
88
import { TodoItemComponent } from '../../../ui/todo-item/todo-item.component';
99
import { TodoSkeletonComponent } from '../../../ui/todo-skeleton/todo-skeleton.component';
@@ -13,15 +13,23 @@ import { TodoSkeletonComponent } from '../../../ui/todo-skeleton/todo-skeleton.c
1313
imports: [
1414
MatInput,
1515
MatFormField,
16+
MatProgressSpinnerModule,
1617
TodoItemComponent,
1718
TodoSkeletonComponent,
18-
MatProgressSpinnerModule,
1919
],
2020
template: `
2121
<h2>Basic</h2>
2222
<div class="mt-8 flex flex-col gap-8">
2323
<div class="flex flex-col gap-4">
24-
<h3>Todo list</h3>
24+
<div class="flex items-center justify-between">
25+
<h3>Todo list</h3>
26+
@if (todos.loading()) {
27+
<mat-progress-spinner
28+
diameter="32"
29+
mode="indeterminate"
30+
></mat-progress-spinner>
31+
}
32+
</div>
2533
2634
<mat-form-field appearance="outline">
2735
<input
@@ -36,18 +44,6 @@ import { TodoSkeletonComponent } from '../../../ui/todo-skeleton/todo-skeleton.c
3644
</mat-form-field>
3745
3846
<div class="relative flex flex-col gap-4">
39-
<!-- Spinner for non-initial loads -->
40-
@if (todos.loading() && !todos.loadingInitial()) {
41-
<div
42-
class="bg-opacity-50 absolute inset-0 z-10 flex items-center justify-center bg-white"
43-
>
44-
<mat-progress-spinner
45-
diameter="24"
46-
mode="indeterminate"
47-
></mat-progress-spinner>
48-
</div>
49-
}
50-
5147
@if (todos.loadingInitial()) {
5248
<showcase-todo-skeleton [repeat]="4" />
5349
} @else {
@@ -76,27 +72,22 @@ import { TodoSkeletonComponent } from '../../../ui/todo-skeleton/todo-skeleton.c
7672
changeDetection: ChangeDetectionStrategy.OnPush,
7773
})
7874
export class BasicComponent {
79-
todos = restResource<Todo, string>('todos', {
80-
// Added back local restResource
81-
create: {
82-
strategy: 'incremental',
83-
},
84-
});
75+
todos = restResource<Todo, string>('todos');
8576
newTodo = signal('');
8677

8778
createTodo() {
8879
const newTodoValue = this.newTodo();
8980
if (newTodoValue.length > 0) {
90-
this.todos.create({ description: newTodoValue, completed: false }); // Changed to todos.create
81+
this.todos.create({ description: newTodoValue, completed: false });
9182
}
9283
this.newTodo.set('');
9384
}
9485

9586
handleToggle(todo: Todo) {
96-
this.todos.update({ ...todo, completed: !todo.completed }); // Changed to todos.update
87+
this.todos.update({ ...todo, completed: !todo.completed });
9788
}
9889

9990
handleRemove(todo: Todo) {
100-
this.todos.remove(todo); // Changed to todos.remove
91+
this.todos.remove(todo);
10192
}
10293
}

0 commit comments

Comments
 (0)