Skip to content

Answer:5 #1219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ec5a610
feat: todo model
kcharles52 Feb 8, 2025
e7d2e42
feat: todo item component
kcharles52 Feb 8, 2025
8f283bd
feat: todo list component
kcharles52 Feb 8, 2025
099d7c5
feat: todo service
kcharles52 Feb 8, 2025
0637d17
fix: remove function paremeter
kcharles52 Feb 8, 2025
d549da0
fix: to initialized later
kcharles52 Feb 8, 2025
f8775d4
fix: use todo service
kcharles52 Feb 8, 2025
b78aade
refactor: use created app todo list component
kcharles52 Feb 8, 2025
911fd06
feat: handleError function and loading and erromessages getters
kcharles52 Feb 8, 2025
dbd5547
feat: add loading and error handling function
kcharles52 Feb 8, 2025
8173bfb
feat: add delete function to todo service
kcharles52 Feb 8, 2025
91f9f17
feat: add delete functionality
kcharles52 Feb 8, 2025
9025cf2
feat: display error and loader
kcharles52 Feb 8, 2025
a1bc139
feat: test fetch todo function
kcharles52 Feb 8, 2025
3603569
feat: test should update todo
kcharles52 Feb 8, 2025
dc4cc5e
feat: subscribe to events of service functions
kcharles52 Feb 8, 2025
a308f4b
feat: listen to delete event
kcharles52 Feb 8, 2025
0bff7c3
test: should delete todo
kcharles52 Feb 9, 2025
f650eb4
test: handle error in todo service
kcharles52 Feb 9, 2025
2582b20
test: initial todoitemcomponent test setup
kcharles52 Feb 9, 2025
fc8805d
test: render todo title
kcharles52 Feb 9, 2025
3a6958c
test: emitting update event on clicking update button
kcharles52 Feb 9, 2025
bd335bf
test: emitting delete event on clicking delete button
kcharles52 Feb 9, 2025
6feb86f
test: get todos on initialization
kcharles52 Feb 9, 2025
c0015ef
test: call updateTodo when item is updated
kcharles52 Feb 10, 2025
64ab487
test: call delete todo when todo is deleted
kcharles52 Feb 10, 2025
8d639e0
chore: install store
kcharles52 Feb 10, 2025
ee741ec
feat: todo store
kcharles52 Feb 10, 2025
88a2920
feat: integrate store
kcharles52 Feb 10, 2025
a28269c
refactor: remove annotation
kcharles52 Feb 10, 2025
9326bbb
refactor: put css file in correct folder
kcharles52 Feb 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 5 additions & 42 deletions apps/angular/5-crud-application/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,13 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { randText } from '@ngneat/falso';
import { Component } from '@angular/core';
import { TodoListComponent } from './pages/todo-list/todo-list.component';

@Component({
imports: [CommonModule],
imports: [CommonModule, TodoListComponent],
selector: 'app-root',
template: `
<div *ngFor="let todo of todos">
{{ todo.title }}
<button (click)="update(todo)">Update</button>
</div>
<app-todo-list />
`,
styles: [],
})
export class AppComponent implements OnInit {
todos!: any[];

constructor(private http: HttpClient) {}

ngOnInit(): void {
this.http
.get<any[]>('https://jsonplaceholder.typicode.com/todos')
.subscribe((todos) => {
this.todos = todos;
});
}

update(todo: any) {
this.http
.put<any>(
`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;
});
}
}
export class AppComponent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div class="todo-item">
<span>{{ todo.title }}</span>
<button (click)="update.emit(todo)" [disabled]="isProcessing">Update</button>
<button (click)="delete.emit(todo)" [disabled]="isProcessing">Delete</button>
<mat-progress-spinner *ngIf="isProcessing" diameter="20"></mat-progress-spinner>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Todo } from '../../models/todo.model';
import { TodoItemComponent } from './todo-item.component';

describe('TodoItemComponent', () => {
let component: TodoItemComponent;
let fixture: ComponentFixture<TodoItemComponent>;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [TodoItemComponent],
}).compileComponents();

fixture = TestBed.createComponent(TodoItemComponent);
component = fixture.componentInstance;
});

it('should render the todo title', () => {
const mockTodo: Todo = {
id: 1,
title: 'Test Todo',
completed: false,
userId: 1,
};
component.todo = mockTodo;

fixture.detectChanges();

const todoText = fixture.nativeElement.querySelector('span').textContent;
expect(todoText).toContain('Test Todo');
});

it('should emit update event on clicking update button', () => {
const mockTodo: Todo = {
id: 1,
title: 'Test Todo',
completed: false,
userId: 1,
};
component.todo = mockTodo;
fixture.detectChanges();

jest.spyOn(component.update, 'emit');

const button = fixture.debugElement.query(By.css('button:nth-of-type(1)'));
button.triggerEventHandler('click', null);

expect(component.update.emit).toHaveBeenCalledWith(mockTodo);
});

it('should emit delete event on clicking delete button', () => {
const mockTodo: Todo = {
id: 1,
title: 'Test Todo',
completed: false,
userId: 1,
};
component.todo = mockTodo;
fixture.detectChanges();

jest.spyOn(component.delete, 'emit');

const button = fixture.debugElement.query(By.css('button:nth-of-type(2)'));
button.triggerEventHandler('click', null);

expect(component.delete.emit).toHaveBeenCalledWith(mockTodo);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Todo } from '../../models/todo.model';

@Component({
selector: 'app-todo-item',
standalone: true,
imports: [CommonModule, MatProgressSpinnerModule],
templateUrl: './todo-item.component.html',
styleUrls: ['./todo-item.component.scss'],
})
export class TodoItemComponent {
@Input()
todo!: Todo;
@Input() loadingIds!: number[];
@Input() isProcessing!: boolean;
@Output() update = new EventEmitter<Todo>();
@Output() delete = new EventEmitter<Todo>();

updateTodo() {
this.update.emit(this.todo);
}

deleteTodo() {
this.delete.emit(this.todo);
}
}
6 changes: 6 additions & 0 deletions apps/angular/5-crud-application/src/app/models/todo.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface Todo {
id: number;
title: string;
completed: boolean;
userId: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<app-loading-spinner *ngIf="(loadingIds$ | async)?.length??0 > 0"></app-loading-spinner>

<div *ngIf="(error$ | async) as errorMessage" class="error-message">
{{ errorMessage }}
</div>

<div *ngIf="(todos$ | async)?.length??0 > 0; else noTodos">
<app-todo-item
*ngFor="let todo of (todos$ | async)"
[todo]="todo"
(update)="update($event)"
(delete)="delete($event)"
[loadingIds]="(loadingIds$ | async) ?? []"
[isProcessing]="isProcessing(todo.id)">
</app-todo-item>
</div>

<ng-template #noTodos>
<p>No todos found</p>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.loading-overlay {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}

.error-message {
color: red;
text-align: center;
margin: 10px 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { of } from 'rxjs';
import { TodoItemComponent } from '../../components/todo-item/todo-item.component';
import { Todo } from '../../models/todo.model';
import { TodoService } from '../../services/todo.service';
import { TodoListComponent } from './todo-list.component';

describe('TodoListComponent', () => {
let component: TodoListComponent;
let fixture: ComponentFixture<TodoListComponent>;
let todoService: TodoService;
let httpMock: HttpTestingController;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
TodoListComponent,
MatProgressSpinnerModule,
TodoItemComponent,
HttpClientTestingModule,
],
providers: [TodoService],
}).compileComponents();
httpMock = TestBed.inject(HttpTestingController);

fixture = TestBed.createComponent(TodoListComponent);
component = fixture.componentInstance;
todoService = TestBed.inject(TodoService);
jest.spyOn(todoService, 'getTodos').mockReturnValue(of([]));
});

it('should call get todos on initialization', () => {
const mockTodos: Todo[] = [
{ id: 1, title: 'Test Todo', completed: false, userId: 1 },
];
jest.spyOn(todoService, 'getTodos').mockReturnValue(of(mockTodos));

fixture.detectChanges();

expect(todoService.getTodos).toHaveBeenCalled();
});

it('should call updateTodo when an item is updated', () => {
const mockTodo: Todo = {
id: 1,
title: 'Test Todo',
completed: false,
userId: 1,
};

jest.spyOn(todoService, 'updateTodo').mockReturnValue(of(mockTodo));
component.update(mockTodo);
expect(todoService.updateTodo).toHaveBeenCalledWith(mockTodo);
});

it('should call deleteTodo when an item is deleted', () => {
const mockTodo: Todo = {
id: 1,
title: 'Test Todo',
completed: false,
userId: 1,
};

jest.spyOn(todoService, 'deleteTodo').mockReturnValue(of(mockTodo));
component.delete(mockTodo);
expect(todoService.deleteTodo).toHaveBeenCalledWith(mockTodo);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Component, OnInit, inject, signal } from '@angular/core';

import { CommonModule } from '@angular/common';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { TodoItemComponent } from '../../components/todo-item/todo-item.component';
import { Todo } from '../../models/todo.model';
import { LoadingSpinnerComponent } from '../../shared/loading-spinner/loading-spinner.component';
import { TodoStore } from '../../stores/todo.store';

@Component({
selector: 'app-todo-list',
standalone: true,
imports: [
CommonModule,
TodoItemComponent,
MatProgressSpinnerModule,
LoadingSpinnerComponent,
],
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.scss'],
providers: [TodoStore],
})
export class TodoListComponent implements OnInit {
todoStore = inject(TodoStore);
todos$ = this.todoStore.todos$;
loadingIds$ = this.todoStore.loadingIds$;
error$ = this.todoStore.error$;
processingId = signal<number | null>(null);

ngOnInit(): void {
this.todoStore.loadTodos();
}

update(todo: Todo) {
this.processingId.set(todo.id);
this.todoStore.updateTodo(todo);
}

delete(todo: Todo) {
this.processingId.set(todo.id);
this.todoStore.deleteTodo(todo);
}

isProcessing = (id: number) => this.processingId() === id;
}
Loading