From 273e969c219c7702e77ef991457aa55f7ad55c3c Mon Sep 17 00:00:00 2001 From: hemadrikhandelwal Date: Tue, 21 Apr 2026 18:26:08 +0530 Subject: [PATCH] feat: crud application --- .../src/app/app.component.scss | 49 ++++++++++ .../src/app/app.component.ts | 69 +++++++------- .../app/data-access/fake-http.service.spec.ts | 95 +++++++++++++++++++ .../src/app/data-access/fake-http.service.ts | 60 ++++++++++++ .../src/app/model/todo.model.ts | 5 + apps/angular/5-crud-application/tsconfig.json | 3 + tsconfig.base.json | 1 + 7 files changed, 248 insertions(+), 34 deletions(-) create mode 100644 apps/angular/5-crud-application/src/app/app.component.scss create mode 100644 apps/angular/5-crud-application/src/app/data-access/fake-http.service.spec.ts create mode 100644 apps/angular/5-crud-application/src/app/data-access/fake-http.service.ts create mode 100644 apps/angular/5-crud-application/src/app/model/todo.model.ts diff --git a/apps/angular/5-crud-application/src/app/app.component.scss b/apps/angular/5-crud-application/src/app/app.component.scss new file mode 100644 index 000000000..391039033 --- /dev/null +++ b/apps/angular/5-crud-application/src/app/app.component.scss @@ -0,0 +1,49 @@ +.table-design { + margin: 20px; + +} + +table { + width: 100%; + border-collapse: collapse; + margin: 20px 0; + +} + +th, td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #ddd; +} + +th { + background-color: #f2f2f2; + font-weight: bold; +} + +tr:hover { + background-color: #f5f5f5; +} + +button { + padding: 8px 16px; + margin: 0 4px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +button:first-of-type { + background-color: #4CAF50; + color: white; +} + +button:last-of-type { + background-color: #f44336; + color: white; +} + +button:hover { + opacity: 0.8; +} \ No newline at end of file diff --git a/apps/angular/5-crud-application/src/app/app.component.ts b/apps/angular/5-crud-application/src/app/app.component.ts index 73ba0dc34..d33b22606 100644 --- a/apps/angular/5-crud-application/src/app/app.component.ts +++ b/apps/angular/5-crud-application/src/app/app.component.ts @@ -1,49 +1,50 @@ -import { HttpClient } from '@angular/common/http'; import { Component, inject, OnInit } from '@angular/core'; -import { randText } from '@ngneat/falso'; + +import { FakeHttpService } from './data-access/fake-http.service'; +import { TODO } from './model/todo.model'; @Component({ imports: [], selector: 'app-root', template: ` - @for (todo of todos; track todo.id) { - {{ todo.title }} - - } +
+ + + + + + + + + + @for (todo of todos(); track todo.id) { + + + + + + } + +
TitleUpdateDelete
{{ todo.title }} + + + +
`, - styles: [], + styleUrls: ['./app.component.scss'], }) export class AppComponent implements OnInit { - private http = inject(HttpClient); - - todos!: any[]; + fakehttpService = inject(FakeHttpService); + readonly todos = this.fakehttpService.todoSignal; ngOnInit(): void { - this.http - .get('https://jsonplaceholder.typicode.com/todos') - .subscribe((todos) => { - this.todos = todos; - }); + this.fakehttpService.getAllTodos(); } - update(todo: any) { - this.http - .put( - `https://jsonplaceholder.typicode.com/todos/${todo.id}`, - JSON.stringify({ - todo: todo.id, - title: randText(), - body: todo.body, - userId: todo.userId, - }), - { - headers: { - 'Content-type': 'application/json; charset=UTF-8', - }, - }, - ) - .subscribe((todoUpdated: any) => { - this.todos[todoUpdated.id - 1] = todoUpdated; - }); + update(todo: TODO) { + this.fakehttpService.updateTodo(todo); + } + delete(todo: TODO) { + this.fakehttpService.deleteTodo(todo); } } diff --git a/apps/angular/5-crud-application/src/app/data-access/fake-http.service.spec.ts b/apps/angular/5-crud-application/src/app/data-access/fake-http.service.spec.ts new file mode 100644 index 000000000..e04494ec1 --- /dev/null +++ b/apps/angular/5-crud-application/src/app/data-access/fake-http.service.spec.ts @@ -0,0 +1,95 @@ +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; + +import { FakeHttpService } from './fake-http.service'; + +describe('FakeHTTPService', () => { + let service: FakeHttpService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + service = TestBed.inject(FakeHttpService); + httpMock = TestBed.inject(HttpTestingController); + }); + + test('should be created', () => { + expect(service).toBeTruthy(); + }); + test('should fetch all todos and update signals', () => { + // Arrange + const mockTodos = [ + { + id: 1, + title: 'Test todo', + body: 'Todo body', + completed: 'false', + }, + ]; + + // Act + service.getAllTodos(); + + // Assert request + const req = httpMock.expectOne( + 'https://jsonplaceholder.typicode.com/todos', + ); + expect(req.request.method).toBe('GET'); + req.flush(mockTodos); + + // Assert signal update + expect(service.todoSignal()).toEqual(mockTodos); + }); + + test('should pass updated todo and update signal', () => { + // Arrange + const initialTodo = [ + { + id: 1, + title: 'old', + completed: false, + }, + ]; + service.todoSignal.set(initialTodo); + + const updatedTodo = { + id: 1, + title: 'new', + completed: true, + }; + + service.updateTodo(updatedTodo); + + const req = httpMock.expectOne( + 'https://jsonplaceholder.typicode.com/todos/1', + ); + expect(req.request.method).toBe('PUT'); + req.flush(updatedTodo); + + expect(service.todoSignal()[0]).toEqual(updatedTodo); + }); + + test('should delete the delted todo and update the signal', () => { + const initalTodo = [ + { + id: 1, + title: 'old', + completed: false, + }, + ]; + service.todoSignal.set(initalTodo); + service.deleteTodo(initalTodo[0]); + + const req = httpMock.expectOne( + 'https://jsonplaceholder.typicode.com/todos/1', + ); + expect(req.request.method).toBe('DELETE'); + req.flush({}); + expect(service.todoSignal().length).toBe(0); + }); +}); diff --git a/apps/angular/5-crud-application/src/app/data-access/fake-http.service.ts b/apps/angular/5-crud-application/src/app/data-access/fake-http.service.ts new file mode 100644 index 000000000..0b744fffc --- /dev/null +++ b/apps/angular/5-crud-application/src/app/data-access/fake-http.service.ts @@ -0,0 +1,60 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable, signal } from '@angular/core'; +import { randText } from '@ngneat/falso'; +import { TODO } from '../model/todo.model'; + +@Injectable({ + providedIn: 'root', +}) +export class FakeHttpService { + private http = inject(HttpClient); + todoSignal = signal([]); + + getAllTodos() { + this.http + .get('https://jsonplaceholder.typicode.com/todos') + .subscribe({ + next: (todosResponse: TODO[]) => { + this.todoSignal.set(todosResponse); + }, + error: (err) => { + console.error('Failed to load todos:', err); + }, + }); + } + + updateTodo(todo: TODO) { + this.http + .put( + `https://jsonplaceholder.typicode.com/todos/${todo.id}`, + { + ...todo, + title: randText(), + }, + { + headers: { + 'Content-type': 'application/json; charset=UTF-8', + }, + }, + ) + .subscribe({ + next: (updated) => { + this.todoSignal.update((todos) => + todos.map((t) => (t.id === updated.id ? updated : t)), + ); + }, + error: (err) => { + console.error('Failed to load todos:', err); + }, + }); + } + deleteTodo(todo: TODO) { + this.http + .delete(`https://jsonplaceholder.typicode.com/todos/${todo.id}`) + .subscribe(() => + this.todoSignal.update((todos) => + todos.filter((t) => t.id !== todo.id), + ), + ); + } +} diff --git a/apps/angular/5-crud-application/src/app/model/todo.model.ts b/apps/angular/5-crud-application/src/app/model/todo.model.ts new file mode 100644 index 000000000..6092bb065 --- /dev/null +++ b/apps/angular/5-crud-application/src/app/model/todo.model.ts @@ -0,0 +1,5 @@ +export interface TODO { + id: number; + title: string; + completed: boolean; +} diff --git a/apps/angular/5-crud-application/tsconfig.json b/apps/angular/5-crud-application/tsconfig.json index a7033d03a..6c490f80c 100644 --- a/apps/angular/5-crud-application/tsconfig.json +++ b/apps/angular/5-crud-application/tsconfig.json @@ -5,6 +5,9 @@ "references": [ { "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" } ], "compilerOptions": { diff --git a/tsconfig.base.json b/tsconfig.base.json index 53c7f6057..56390c7a0 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -14,6 +14,7 @@ "skipLibCheck": true, "skipDefaultLibCheck": true, "baseUrl": ".", + "ignoreDeprecations": "6.0", "paths": { "@angular-challenges/cli": ["libs/cli/src/index.ts"], "@angular-challenges/custom-plugin": ["libs/custom-plugin/src/index.ts"],