Skip to content

Commit 273e969

Browse files
feat: crud application
1 parent 543770b commit 273e969

7 files changed

Lines changed: 248 additions & 34 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
.table-design {
2+
margin: 20px;
3+
4+
}
5+
6+
table {
7+
width: 100%;
8+
border-collapse: collapse;
9+
margin: 20px 0;
10+
11+
}
12+
13+
th, td {
14+
padding: 12px;
15+
text-align: left;
16+
border-bottom: 1px solid #ddd;
17+
}
18+
19+
th {
20+
background-color: #f2f2f2;
21+
font-weight: bold;
22+
}
23+
24+
tr:hover {
25+
background-color: #f5f5f5;
26+
}
27+
28+
button {
29+
padding: 8px 16px;
30+
margin: 0 4px;
31+
border: none;
32+
border-radius: 4px;
33+
cursor: pointer;
34+
font-size: 14px;
35+
}
36+
37+
button:first-of-type {
38+
background-color: #4CAF50;
39+
color: white;
40+
}
41+
42+
button:last-of-type {
43+
background-color: #f44336;
44+
color: white;
45+
}
46+
47+
button:hover {
48+
opacity: 0.8;
49+
}
Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,50 @@
1-
import { HttpClient } from '@angular/common/http';
21
import { Component, inject, OnInit } from '@angular/core';
3-
import { randText } from '@ngneat/falso';
2+
3+
import { FakeHttpService } from './data-access/fake-http.service';
4+
import { TODO } from './model/todo.model';
45

56
@Component({
67
imports: [],
78
selector: 'app-root',
89
template: `
9-
@for (todo of todos; track todo.id) {
10-
{{ todo.title }}
11-
<button (click)="update(todo)">Update</button>
12-
}
10+
<div class="table-design"></div>
11+
<table border="1">
12+
<thead>
13+
<tr>
14+
<th>Title</th>
15+
<th>Update</th>
16+
<th>Delete</th>
17+
</tr>
18+
</thead>
19+
<tbody>
20+
@for (todo of todos(); track todo.id) {
21+
<tr>
22+
<td>{{ todo.title }}</td>
23+
<td>
24+
<button (click)="update(todo)">Update</button>
25+
</td>
26+
<td>
27+
<button (click)="delete(todo)">Delete</button>
28+
</td>
29+
</tr>
30+
}
31+
</tbody>
32+
</table>
1333
`,
14-
styles: [],
34+
styleUrls: ['./app.component.scss'],
1535
})
1636
export class AppComponent implements OnInit {
17-
private http = inject(HttpClient);
18-
19-
todos!: any[];
37+
fakehttpService = inject(FakeHttpService);
38+
readonly todos = this.fakehttpService.todoSignal;
2039

2140
ngOnInit(): void {
22-
this.http
23-
.get<any[]>('https://jsonplaceholder.typicode.com/todos')
24-
.subscribe((todos) => {
25-
this.todos = todos;
26-
});
41+
this.fakehttpService.getAllTodos();
2742
}
2843

29-
update(todo: any) {
30-
this.http
31-
.put<any>(
32-
`https://jsonplaceholder.typicode.com/todos/${todo.id}`,
33-
JSON.stringify({
34-
todo: todo.id,
35-
title: randText(),
36-
body: todo.body,
37-
userId: todo.userId,
38-
}),
39-
{
40-
headers: {
41-
'Content-type': 'application/json; charset=UTF-8',
42-
},
43-
},
44-
)
45-
.subscribe((todoUpdated: any) => {
46-
this.todos[todoUpdated.id - 1] = todoUpdated;
47-
});
44+
update(todo: TODO) {
45+
this.fakehttpService.updateTodo(todo);
46+
}
47+
delete(todo: TODO) {
48+
this.fakehttpService.deleteTodo(todo);
4849
}
4950
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
HttpClientTestingModule,
3+
HttpTestingController,
4+
} from '@angular/common/http/testing';
5+
import { TestBed } from '@angular/core/testing';
6+
7+
import { FakeHttpService } from './fake-http.service';
8+
9+
describe('FakeHTTPService', () => {
10+
let service: FakeHttpService;
11+
let httpMock: HttpTestingController;
12+
13+
beforeEach(() => {
14+
TestBed.configureTestingModule({
15+
imports: [HttpClientTestingModule],
16+
});
17+
service = TestBed.inject(FakeHttpService);
18+
httpMock = TestBed.inject(HttpTestingController);
19+
});
20+
21+
test('should be created', () => {
22+
expect(service).toBeTruthy();
23+
});
24+
test('should fetch all todos and update signals', () => {
25+
// Arrange
26+
const mockTodos = [
27+
{
28+
id: 1,
29+
title: 'Test todo',
30+
body: 'Todo body',
31+
completed: 'false',
32+
},
33+
];
34+
35+
// Act
36+
service.getAllTodos();
37+
38+
// Assert request
39+
const req = httpMock.expectOne(
40+
'https://jsonplaceholder.typicode.com/todos',
41+
);
42+
expect(req.request.method).toBe('GET');
43+
req.flush(mockTodos);
44+
45+
// Assert signal update
46+
expect(service.todoSignal()).toEqual(mockTodos);
47+
});
48+
49+
test('should pass updated todo and update signal', () => {
50+
// Arrange
51+
const initialTodo = [
52+
{
53+
id: 1,
54+
title: 'old',
55+
completed: false,
56+
},
57+
];
58+
service.todoSignal.set(initialTodo);
59+
60+
const updatedTodo = {
61+
id: 1,
62+
title: 'new',
63+
completed: true,
64+
};
65+
66+
service.updateTodo(updatedTodo);
67+
68+
const req = httpMock.expectOne(
69+
'https://jsonplaceholder.typicode.com/todos/1',
70+
);
71+
expect(req.request.method).toBe('PUT');
72+
req.flush(updatedTodo);
73+
74+
expect(service.todoSignal()[0]).toEqual(updatedTodo);
75+
});
76+
77+
test('should delete the delted todo and update the signal', () => {
78+
const initalTodo = [
79+
{
80+
id: 1,
81+
title: 'old',
82+
completed: false,
83+
},
84+
];
85+
service.todoSignal.set(initalTodo);
86+
service.deleteTodo(initalTodo[0]);
87+
88+
const req = httpMock.expectOne(
89+
'https://jsonplaceholder.typicode.com/todos/1',
90+
);
91+
expect(req.request.method).toBe('DELETE');
92+
req.flush({});
93+
expect(service.todoSignal().length).toBe(0);
94+
});
95+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { HttpClient } from '@angular/common/http';
2+
import { inject, Injectable, signal } from '@angular/core';
3+
import { randText } from '@ngneat/falso';
4+
import { TODO } from '../model/todo.model';
5+
6+
@Injectable({
7+
providedIn: 'root',
8+
})
9+
export class FakeHttpService {
10+
private http = inject(HttpClient);
11+
todoSignal = signal<TODO[]>([]);
12+
13+
getAllTodos() {
14+
this.http
15+
.get<TODO[]>('https://jsonplaceholder.typicode.com/todos')
16+
.subscribe({
17+
next: (todosResponse: TODO[]) => {
18+
this.todoSignal.set(todosResponse);
19+
},
20+
error: (err) => {
21+
console.error('Failed to load todos:', err);
22+
},
23+
});
24+
}
25+
26+
updateTodo(todo: TODO) {
27+
this.http
28+
.put<TODO>(
29+
`https://jsonplaceholder.typicode.com/todos/${todo.id}`,
30+
{
31+
...todo,
32+
title: randText(),
33+
},
34+
{
35+
headers: {
36+
'Content-type': 'application/json; charset=UTF-8',
37+
},
38+
},
39+
)
40+
.subscribe({
41+
next: (updated) => {
42+
this.todoSignal.update((todos) =>
43+
todos.map((t) => (t.id === updated.id ? updated : t)),
44+
);
45+
},
46+
error: (err) => {
47+
console.error('Failed to load todos:', err);
48+
},
49+
});
50+
}
51+
deleteTodo(todo: TODO) {
52+
this.http
53+
.delete<TODO>(`https://jsonplaceholder.typicode.com/todos/${todo.id}`)
54+
.subscribe(() =>
55+
this.todoSignal.update((todos) =>
56+
todos.filter((t) => t.id !== todo.id),
57+
),
58+
);
59+
}
60+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface TODO {
2+
id: number;
3+
title: string;
4+
completed: boolean;
5+
}

apps/angular/5-crud-application/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
"references": [
66
{
77
"path": "./tsconfig.app.json"
8+
},
9+
{
10+
"path": "./tsconfig.spec.json"
811
}
912
],
1013
"compilerOptions": {

tsconfig.base.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"skipLibCheck": true,
1515
"skipDefaultLibCheck": true,
1616
"baseUrl": ".",
17+
"ignoreDeprecations": "6.0",
1718
"paths": {
1819
"@angular-challenges/cli": ["libs/cli/src/index.ts"],
1920
"@angular-challenges/custom-plugin": ["libs/custom-plugin/src/index.ts"],

0 commit comments

Comments
 (0)