diff --git a/apps/angular/crud/src/app/app.component.ts b/apps/angular/crud/src/app/app.component.ts
index 8c3d1b8ae..07c240ca4 100644
--- a/apps/angular/crud/src/app/app.component.ts
+++ b/apps/angular/crud/src/app/app.component.ts
@@ -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: `
-
- {{ todo.title }}
-
-
+ @if (queryClient.isFetching()) {
+
+ }
+
`,
styles: [],
})
-export class AppComponent implements OnInit {
- todos!: any[];
-
- constructor(private http: HttpClient) {}
-
- ngOnInit(): void {
- this.http
- .get('https://jsonplaceholder.typicode.com/todos')
- .subscribe((todos) => {
- this.todos = todos;
- });
- }
-
- 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;
- });
- }
+export class AppComponent {
+ public queryClient = injectQueryClient();
}
diff --git a/apps/angular/crud/src/app/app.config.ts b/apps/angular/crud/src/app/app.config.ts
index de0a3ccec..796bc205c 100644
--- a/apps/angular/crud/src/app/app.config.ts
+++ b/apps/angular/crud/src/app/app.config.ts
@@ -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 },
+ ],
};
diff --git a/apps/angular/crud/src/app/components/todos/todos.component.css b/apps/angular/crud/src/app/components/todos/todos.component.css
new file mode 100644
index 000000000..6b61a4cb2
--- /dev/null
+++ b/apps/angular/crud/src/app/components/todos/todos.component.css
@@ -0,0 +1,8 @@
+.todos{
+ list-style:decimal;
+}
+.todo{
+ padding: 0.5rem;
+ border-bottom: 2px solid salmon;
+ width: fit-content;
+}
\ No newline at end of file
diff --git a/apps/angular/crud/src/app/components/todos/todos.component.html b/apps/angular/crud/src/app/components/todos/todos.component.html
new file mode 100644
index 000000000..ddb94754c
--- /dev/null
+++ b/apps/angular/crud/src/app/components/todos/todos.component.html
@@ -0,0 +1,14 @@
+@if (todoStore.query.isFetched()) {
+
+ @for (todo of todoStore.query.data(); track todo.title) {
+ -
+ {{ todo.title }}
+
+
+
+ }
+
+}
+@if(todoStore.query.error()){
+An error occured..
+}
\ No newline at end of file
diff --git a/apps/angular/crud/src/app/components/todos/todos.component.spec.ts b/apps/angular/crud/src/app/components/todos/todos.component.spec.ts
new file mode 100644
index 000000000..617ecd7c5
--- /dev/null
+++ b/apps/angular/crud/src/app/components/todos/todos.component.spec.ts
@@ -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;
+ 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,
+ });
+ });
+});
diff --git a/apps/angular/crud/src/app/components/todos/todos.component.ts b/apps/angular/crud/src/app/components/todos/todos.component.ts
new file mode 100644
index 000000000..b90319a2c
--- /dev/null
+++ b/apps/angular/crud/src/app/components/todos/todos.component.ts
@@ -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 });
+ }
+}
diff --git a/apps/angular/crud/src/app/interfaces/todo.interface.ts b/apps/angular/crud/src/app/interfaces/todo.interface.ts
new file mode 100644
index 000000000..2a4adb4d6
--- /dev/null
+++ b/apps/angular/crud/src/app/interfaces/todo.interface.ts
@@ -0,0 +1,6 @@
+export interface Todo {
+ id: number;
+ title: string;
+ completed?: boolean;
+ userId?: number;
+}
diff --git a/apps/angular/crud/src/app/services/error-handler.service.ts b/apps/angular/crud/src/app/services/error-handler.service.ts
new file mode 100644
index 000000000..8a0d946a4
--- /dev/null
+++ b/apps/angular/crud/src/app/services/error-handler.service.ts
@@ -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);
+ }
+}
diff --git a/apps/angular/crud/src/app/services/todo.service.spec.ts b/apps/angular/crud/src/app/services/todo.service.spec.ts
new file mode 100644
index 000000000..d824713f9
--- /dev/null
+++ b/apps/angular/crud/src/app/services/todo.service.spec.ts
@@ -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()}`,
+ );
+ });
+ });
+ });
+});
diff --git a/apps/angular/crud/src/app/services/todo.service.ts b/apps/angular/crud/src/app/services/todo.service.ts
new file mode 100644
index 000000000..b409a35d7
--- /dev/null
+++ b/apps/angular/crud/src/app/services/todo.service.ts
@@ -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 {
+ // Added 1s fake delay
+ return this.http.get(BASE_URL).pipe(delay(1000));
+ }
+
+ update(todo: Todo) {
+ const headerOption = {
+ headers: {
+ 'Content-type': 'application/json; charset=UTF-8',
+ },
+ };
+
+ return this.http.put(
+ `${BASE_URL}/${todo.id}`,
+ JSON.stringify(todo),
+ headerOption,
+ );
+ }
+
+ delete(todoId: number) {
+ return this.http.delete(`${BASE_URL}/${todoId.toString()}`);
+ }
+}
diff --git a/apps/angular/crud/src/app/store/enums/actions.enum.ts b/apps/angular/crud/src/app/store/enums/actions.enum.ts
new file mode 100644
index 000000000..2b70229e9
--- /dev/null
+++ b/apps/angular/crud/src/app/store/enums/actions.enum.ts
@@ -0,0 +1,5 @@
+export enum OperationType {
+ UPDATE = 'UPDATE',
+ DELETE = 'DELETE',
+ GET = 'GET',
+}
diff --git a/apps/angular/crud/src/app/store/todo/todo-args.ts b/apps/angular/crud/src/app/store/todo/todo-args.ts
new file mode 100644
index 000000000..e9b52651a
--- /dev/null
+++ b/apps/angular/crud/src/app/store/todo/todo-args.ts
@@ -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;
diff --git a/apps/angular/crud/src/app/store/todo/todo-store.ts b/apps/angular/crud/src/app/store/todo/todo-store.ts
new file mode 100644
index 000000000..3b2ff4c7b
--- /dev/null
+++ b/apps/angular/crud/src/app/store/todo/todo-store.ts
@@ -0,0 +1,77 @@
+import { Injectable, inject } from '@angular/core';
+
+import {
+ injectMutation,
+ injectQuery,
+ injectQueryClient,
+} from '@tanstack/angular-query-experimental';
+import { Todo } from '../../interfaces/todo.interface';
+import { TodoService } from '../../services/todo.service';
+import { OperationType } from '../enums/actions.enum';
+import { TodoStateArgs } from './todo-args';
+@Injectable({
+ providedIn: 'root',
+})
+export class TodoStore {
+ private readonly QUERY_KEY = 'TODOS_STORE';
+ private queryClient = injectQueryClient();
+ private todoService = inject(TodoService);
+
+ public query = injectQuery(() => ({
+ queryKey: [this.QUERY_KEY],
+ queryFn: () => this.todoService.get().toPromise(),
+ }));
+
+ mutation = injectMutation(() => ({
+ mutationFn: async (args: TodoStateArgs) => {
+ switch (args.type) {
+ case OperationType.UPDATE: {
+ return await this.todoService.update(args.payload).toPromise();
+ }
+ case OperationType.DELETE: {
+ return await this.todoService.delete(args.payload).toPromise();
+ }
+ case OperationType.GET: {
+ return await this.todoService.get().toPromise();
+ }
+ }
+ },
+ onSuccess: (data, variables) => {
+ const currentTodos =
+ this.queryClient.getQueryData([this.QUERY_KEY]) ?? [];
+ switch (variables.type) {
+ case OperationType.DELETE: {
+ return this.queryClient.setQueryData(
+ [this.QUERY_KEY],
+ this.deleteTodo(currentTodos, variables.payload),
+ );
+ }
+ case OperationType.UPDATE: {
+ return this.queryClient.setQueryData(
+ [this.QUERY_KEY],
+ this.updateTodo(currentTodos, variables.payload),
+ );
+ }
+ case OperationType.GET: {
+ return this.queryClient.setQueryData([this.QUERY_KEY], data);
+ }
+ }
+ },
+ onError: (error) => {
+ console.error(error);
+ },
+ }));
+
+ private deleteTodo(todos: Todo[], id: number) {
+ return todos.filter((todo) => todo.id !== id);
+ }
+
+ private updateTodo(todos: Todo[], updatedTodo: Todo) {
+ return todos.map((todo) => {
+ if (todo.id === updatedTodo.id) {
+ todo = updatedTodo;
+ }
+ return todo;
+ });
+ }
+}
diff --git a/package.json b/package.json
index 89f6215f7..1363cd133 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
"@rx-angular/state": "^17.0.0",
"@rx-angular/template": "^17.0.0",
"@swc/helpers": "0.5.3",
- "@tanstack/angular-query-experimental": "^5.12.1",
+ "@tanstack/angular-query-experimental": "^5.28.9",
"rxjs": "7.8.1",
"tailwindcss": "^3.3.5",
"tslib": "^2.3.0",