Skip to content

Commit

Permalink
feat: implement tanstack store in todos
Browse files Browse the repository at this point in the history
  • Loading branch information
sefatanam committed Apr 14, 2024
1 parent 5413b9e commit bafc423
Show file tree
Hide file tree
Showing 14 changed files with 375 additions and 44 deletions.
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();
}
19 changes: 17 additions & 2 deletions apps/angular/crud/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import { HttpClientModule } from '@angular/common/http';
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import {
ApplicationConfig,
ErrorHandler,
importProvidersFrom,
} 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: [
importProvidersFrom(HttpClientModule),
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;
}
14 changes: 14 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,14 @@
@if (todoStore.query.isFetched()) {
<ol class="todos">
@for (todo of todoStore.query.data(); track todo.title) {
<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.error()){
<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,
});
});
});
27 changes: 27 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,27 @@
import { Component, inject } from '@angular/core';
import { randText } from '@ngneat/falso';
import { Todo } from '../../interfaces/todo.interface';
import { OperationType } from '../../store/enums/actions.enum';
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.mutation.mutate({
type: OperationType.UPDATE,
payload: todo,
});
}

delete(id: number) {
this.todoStore.mutation.mutate({ type: OperationType.DELETE, 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(1000));
}

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,
);
}

delete(todoId: number) {
return this.http.delete(`${BASE_URL}/${todoId.toString()}`);
}
}
5 changes: 5 additions & 0 deletions apps/angular/crud/src/app/store/enums/actions.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum OperationType {
UPDATE = 'UPDATE',
DELETE = 'DELETE',
GET = 'GET',
}
19 changes: 19 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,19 @@
import { Todo } from '../../interfaces/todo.interface';
import { OperationType } from '../enums/actions.enum';

export interface TodoUpdateArgs {
type: OperationType.UPDATE;
payload: Todo;
}

export interface TodoDeleteArgs {
type: OperationType.DELETE;
payload: number;
}

export interface TodoGethArgs {
type: OperationType.GET;
}

// Define a union type combining both interfaces
export type TodoStateArgs = TodoDeleteArgs | TodoUpdateArgs | TodoGethArgs;
Loading

0 comments on commit bafc423

Please sign in to comment.