Skip to content
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

Answer:5 # feat: implement tanstack query in todos #772

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 11 additions & 41 deletions apps/angular/crud/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,21 @@
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 { MatProgressBarModule } from '@angular/material/progress-bar';
import { injectQueryClient } from '@tanstack/angular-query-experimental';
import { TodosComponent } from './components/todos/todos.component';

@Component({
standalone: true,
imports: [CommonModule],
imports: [CommonModule, MatProgressBarModule, TodosComponent],
selector: 'app-root',
template: `
<div *ngFor="let todo of todos">
{{ todo.title }}
<button (click)="update(todo)">Update</button>
</div>
@if (queryClient.isFetching()) {
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
}
<app-todos />
`,
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 {
public queryClient = injectQueryClient();
}
17 changes: 14 additions & 3 deletions apps/angular/crud/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { HttpClientModule } from '@angular/common/http';
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig, ErrorHandler } from '@angular/core';
import { ErrorHandlerService } from './services/error-handler.service';

import {
provideAngularQuery,
QueryClient,
} from '@tanstack/angular-query-experimental';

export const appConfig: ApplicationConfig = {
providers: [importProvidersFrom(HttpClientModule)],
providers: [
provideHttpClient(),
provideAngularQuery(new QueryClient()),
{ provide: ErrorHandler, useClass: ErrorHandlerService },
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.todos{
list-style:decimal;
}
.todo{
padding: 0.5rem;
border-bottom: 2px solid salmon;
width: fit-content;
}
17 changes: 17 additions & 0 deletions apps/angular/crud/src/app/components/todos/todos.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@if(todoStore.query.isPending()){
<p>Loading.....</p>
}
@if (todoStore.query.isSuccess()) {
<ol class="todos">
@for (todo of todoStore.query.data(); track todo.id) {
<li class="todo" title="{{todo.title}}" id="{{todo.id}}">
{{ todo.title }}
<button (click)="update(todo)">Update</button>
<button (click)="delete(todo.id)">Delete</button>
</li>
}
</ol>
}
@if(todoStore.query.isError()){
<p style="color: red;">An error occured..</p>
}
56 changes: 56 additions & 0 deletions apps/angular/crud/src/app/components/todos/todos.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// todos.component.spec.ts

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Todo } from '../../interfaces/todo.interface';
import { OperationType } from '../../store/enums/actions.enum';
import { TodoStore } from '../../store/todo/todo-store';
import { TodosComponent } from './todos.component';

describe('TodosComponent', () => {
let component: TodosComponent;
let fixture: ComponentFixture<TodosComponent>;
const todoStoreMock = {
query: {
isFetched: jest.fn(),
data: jest.fn(),
error: jest.fn(),
},
mutation: {
mutate: jest.fn(),
},
};

beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [{ provide: TodoStore, useValue: todoStoreMock }],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(TodosComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should call update method with correct payload', () => {
const todo: Todo = { id: 1, title: 'Todo 1' };
component.update(todo);
expect(todoStoreMock.mutation.mutate).toHaveBeenCalledWith({
type: OperationType.UPDATE,
payload: { ...todo, title: expect.any(String) },
});
});

it('should call delete method with correct payload', () => {
const todoId = 1;
component.delete(todoId);
expect(todoStoreMock.mutation.mutate).toHaveBeenCalledWith({
type: OperationType.DELETE,
payload: todoId,
});
});
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice tests 👍

});
25 changes: 25 additions & 0 deletions apps/angular/crud/src/app/components/todos/todos.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Component, inject } from '@angular/core';
import { randText } from '@ngneat/falso';
import { Todo } from '../../interfaces/todo.interface';
import { TodoStore } from '../../store/todo/todo-store';

@Component({
selector: 'app-todos',
standalone: true,
templateUrl: './todos.component.html',
styleUrl: './todos.component.css',
})
export class TodosComponent {
todoStore = inject(TodoStore);

update(todo: Todo) {
todo = { ...todo, title: randText() };
this.todoStore.update.mutate({
payload: todo,
});
}

delete(id: number) {
this.todoStore.delete.mutate({ payload: id });
}
}
6 changes: 6 additions & 0 deletions apps/angular/crud/src/app/interfaces/todo.interface.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;
}
10 changes: 10 additions & 0 deletions apps/angular/crud/src/app/services/error-handler.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ErrorHandler, Injectable } from '@angular/core';

@Injectable({
providedIn: 'root',
})
export class ErrorHandlerService implements ErrorHandler {
handleError(error: unknown): void {
console.error(error);
}
}
89 changes: 89 additions & 0 deletions apps/angular/crud/src/app/services/todo.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { HttpClient } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { Todo } from '../interfaces/todo.interface';
import { TodoService } from './todo.service';

class MockHttpClient {
get = jest.fn();
put = jest.fn();
delete = jest.fn();
}
const httpClientMock = new MockHttpClient();
const BASE_URL = 'api_url';

describe('TodoService', () => {
let service: TodoService;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
TodoService,
{ provide: HttpClient, useValue: httpClientMock },
],
});
service = TestBed.inject(TodoService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});

describe('get method', () => {
it('should return observable of Todo array', () => {
const dummyTodos = [
{ id: 1, title: 'Todo 1' },
{ id: 2, title: 'Todo 2' },
];
httpClientMock.get.mockReturnValue(of(dummyTodos));
service.get().subscribe((todos) => {
expect(todos).toEqual(dummyTodos);
});
});

it('should delay response by 1 second', () => {
const dummyTodos = [
{ id: 1, title: 'Todo 1' },
{ id: 2, title: 'Todo 2' },
];
httpClientMock.get.mockReturnValue(of(dummyTodos).pipe(delay(1000)));
const start = Date.now();
service.get().subscribe(() => {
const end = Date.now();
expect(end - start).toBeGreaterThanOrEqual(1000);
});
});
});

describe('update method', () => {
it('should call HttpClient.put with correct URL and payload', () => {
const todo: Todo = {
id: 1,
title: 'Updated Todo',
completed: false,
userId: 1,
};
httpClientMock.put.mockReturnValue(of(todo));
service.update(todo).subscribe(() => {
expect(httpClientMock.put).toHaveBeenCalledWith(
`${BASE_URL}/${todo.id}`,
JSON.stringify(todo),
{ headers: { 'Content-type': 'application/json; charset=UTF-8' } },
);
});
});
});

describe('delete method', () => {
it('should call HttpClient.delete with correct URL', () => {
const todoId = 1;
httpClientMock.delete.mockReturnValue(of(null));
service.delete(todoId).subscribe(() => {
expect(httpClientMock.delete).toHaveBeenCalledWith(
`${BASE_URL}/${todoId.toString()}`,
);
});
});
});
});
35 changes: 35 additions & 0 deletions apps/angular/crud/src/app/services/todo.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable, delay } from 'rxjs';
import { Todo } from '../interfaces/todo.interface';

const BASE_URL = 'https://jsonplaceholder.typicode.com/todos';
@Injectable({
providedIn: 'root',
})
export class TodoService {
private http = inject(HttpClient);

get(): Observable<Todo[]> {
// Added 1s fake delay
return this.http.get<Todo[]>(BASE_URL).pipe(delay(3000));
}

update(todo: Todo) {
const headerOption = {
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
};

return this.http
.put<Todo>(`${BASE_URL}/${todo.id}`, JSON.stringify(todo), headerOption)
.pipe(delay(1000));
}

delete(todoId: number) {
return this.http
.delete(`${BASE_URL}/${todoId.toString()}`)
.pipe(delay(1000));
}
}
9 changes: 9 additions & 0 deletions apps/angular/crud/src/app/store/todo/todo-args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Todo } from '../../interfaces/todo.interface';

export interface TodoUpdateArgs {
payload: Todo;
}

export interface TodoDeleteArgs {
payload: number;
}
Loading
Loading