Skip to content

Answer:5 - crud application using NgRx #1361

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 1 commit into
base: main
Choose a base branch
from
Open
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
70 changes: 33 additions & 37 deletions apps/angular/5-crud-application/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,46 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { randText } from '@ngneat/falso';
import { Component, inject, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppItemComponent } from './todos/components/item.component';
import { AppGlobalLoaderComponent } from './todos/components/loader-global.component';
import { fetchTodos } from './todos/stores/todos.actions';
import {
selectorFetchErroredGlobal,
selectorIsGlobalLoaderVisible,
selectorTodos,
} from './todos/stores/todos.selectors';

@Component({
imports: [CommonModule],
imports: [CommonModule, AppGlobalLoaderComponent, AppItemComponent],
providers: [],
selector: 'app-root',
template: `
<div *ngFor="let todo of todos">
{{ todo.title }}
<button (click)="update(todo)">Update</button>
</div>
@if (hasErroredGlobal()) {
<p>Error fetching todos</p>
} @else if (isGlobalLoaderVisble()) {
<app-global-loader />
} @else {
@for (todo of todos(); track todo.id) {
<app-item [todo]="todo" />
} @empty {
<p>There are no todos...</p>
}
}
`,
styles: [],
})
export class AppComponent implements OnInit {
todos!: any[];

constructor(private http: HttpClient) {}
private readonly store = inject(Store);
public readonly isGlobalLoaderVisble = this.store.selectSignal(
selectorIsGlobalLoaderVisible,
);
public readonly hasErroredGlobal = this.store.selectSignal(
selectorFetchErroredGlobal,
);
public readonly todos = this.store.selectSignal(selectorTodos);

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;
});
console.log('hello');
this.store.dispatch(fetchTodos());
}
}
22 changes: 20 additions & 2 deletions apps/angular/5-crud-application/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig } from '@angular/core';
import { ApplicationConfig, isDevMode } from '@angular/core';
import { provideEffects } from '@ngrx/effects';
import { provideState, provideStore } from '@ngrx/store';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { TodosEffects } from './todos/stores/todos.effects';
import { todosReducer } from './todos/stores/todos.reducer';

export const appConfig: ApplicationConfig = {
providers: [provideHttpClient()],
providers: [
provideHttpClient(),
provideStore(),
provideState({ name: 'todosCrud', reducer: todosReducer }),
provideEffects(TodosEffects),
provideStoreDevtools({
maxAge: 25, // Retains last 25 states
logOnly: !isDevMode(), // Restrict extension to log-only mode
autoPause: true, // Pauses recording actions and state changes when the extension window is not open
trace: false, // If set to true, will include stack trace for every dispatched action, so you can see it in trace tab jumping directly to that part of code
traceLimit: 75, // maximum stack trace frames to be stored (in case trace option was provided as true)
connectInZone: true, // If set to true, the connection is established within the Angular zone
}),
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { CommonModule } from '@angular/common';
import { Component, inject, input } from '@angular/core';
import { Store } from '@ngrx/store';
import { ITodo } from '../interfaces/todo.interface';
import { deleteTodo, updateTodo } from '../stores/todos.actions';
import {
selectorFetchErroredLocal,
selectorFetchErroredLocalMessage,
} from '../stores/todos.selectors';
import { AppLocalLoaderComponent } from './loader-local.component';

@Component({
imports: [CommonModule, AppLocalLoaderComponent],
providers: [],
selector: 'app-item',
template: `
<div>
@if (todo().isLoading) {
<app-local-loader />
} @else if (
hasErroredLocal().status && hasErroredLocal().todoId === todo().id
) {
<p style="color: red;">{{ hasErroredLocalMessage() }}</p>
} @else {
<span>{{ todo().title }}</span>
<button (click)="onUpdateTodo(todo())" [disabled]="todo().isLoading">
Update
</button>
<button (click)="onDeleteTodo(todo().id)" [disabled]="todo().isLoading">
Delete
</button>
}
</div>
`,
styles: `
.isLoading {
background-color: red;
}
`,
})
export class AppItemComponent {
todo = input.required<ITodo>();
private readonly store = inject(Store);
public readonly hasErroredLocal = this.store.selectSignal(
selectorFetchErroredLocal,
);
public readonly hasErroredLocalMessage = this.store.selectSignal(
selectorFetchErroredLocalMessage,
);

onUpdateTodo(todo: ITodo) {
this.store.dispatch(updateTodo({ todo }));
}

onDeleteTodo(id: number) {
this.store.dispatch(deleteTodo({ id }));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';

@Component({
imports: [CommonModule],
selector: 'app-global-loader',
template: `
<div class="loader"><div class="loader_gradient"></div></div>
`,
styles: `
.loader {
width: 100vw;
height: 100vh;
}
.loader_gradient {
position: absolute;
left: 50%;
top: 50%;
width: 50px;
--b: 8px;
aspect-ratio: 1;
border-radius: 50%;
padding: 1px;
background: conic-gradient(#0000 10%, #f03355) content-box;
-webkit-mask: repeating-conic-gradient(
#0000 0deg,
#000 1deg 20deg,
#0000 21deg 36deg
),
radial-gradient(
farthest-side,
#0000 calc(100% - var(--b) - 1px),
#000 calc(100% - var(--b))
);
-webkit-mask-composite: destination-in;
mask-composite: intersect;
animation: l4 1s infinite steps(10);
}
@keyframes l4 {
to {
transform: rotate(1turn);
}
}
`,
})
export class AppGlobalLoaderComponent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';

@Component({
imports: [CommonModule],
selector: 'app-local-loader',
template: `
<div class="loader"><div class="loader_gradient"></div></div>
`,
styles: `
.loader_gradient {
width: 20px;
--b: 8px;
aspect-ratio: 1;
border-radius: 50%;
padding: 1px;
background: conic-gradient(#0000 10%, #f03355) content-box;
-webkit-mask: repeating-conic-gradient(
#0000 0deg,
#000 1deg 20deg,
#0000 21deg 36deg
),
radial-gradient(
farthest-side,
#0000 calc(100% - var(--b) - 1px),
#000 calc(100% - var(--b))
);
-webkit-mask-composite: destination-in;
mask-composite: intersect;
animation: l4 1s infinite steps(10);
}
@keyframes l4 {
to {
transform: rotate(1turn);
}
}
`,
})
export class AppLocalLoaderComponent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface ITodo {
completed: boolean;
title: string;
userId: number;
id: number;
isLoading?: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { randText } from '@ngneat/falso';
import { map, Observable } from 'rxjs';
import { ITodo } from '../interfaces/todo.interface';
import { Todo } from './todos';

@Injectable({
providedIn: 'root',
})
export class TodosService extends Todo {
private http = inject(HttpClient);
private readonly baseUrl = 'https://jsonplaceholder.typicode.com/todos';

getAll(): Observable<ITodo[]> {
return this.http
.get<ITodo[]>(this.baseUrl)
.pipe(
map((todos: ITodo[]) =>
todos.map((todo: ITodo) => ({ ...todo, isLoading: false })),
),
);
}

updateTodo(todo: ITodo): Observable<ITodo> {
return this.http.put<ITodo>(
`${this.baseUrl}/${todo.id}`,
JSON.stringify({
title: randText(),
completed: false,
userId: todo.userId,
id: todo.id,
isLoading: false,
}),
{
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
},
);
}

deleteTodo(id: number): Observable<object> {
return this.http.delete(`${this.baseUrl}/${id}`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Observable } from 'rxjs';
import { ITodo } from '../interfaces/todo.interface';

export abstract class Todo {
abstract getAll(): Observable<ITodo[]>;
abstract updateTodo(todo: ITodo): Observable<ITodo>;
abstract deleteTodo(id: number): Observable<object>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { createAction, props } from '@ngrx/store';
import { ITodo } from '../interfaces/todo.interface';

export const showGlobalLoader = createAction(
'[Todos] Show Global Loader',
props<{ isGlobalLoaderVisible: true }>,
);

export const hideGlobalLoader = createAction(
'[Todos] Hide Global Loader',
props<{ isGlobalLoaderVisible: false }>,
);

export const showLocalLoader = createAction(
'[Todos] Show Local Loader',
props<{ isLocalLoaderVisible: true }>,
);

export const hideLocalLoader = createAction(
'[Todos] Hide Local Loader',
props<{ isLocalLoaderVisible: false }>,
);

export const fetchTodos = createAction('[Todos] Fetch Todos');

export const fetchTodosSuccess = createAction(
'[Todos] Fetch Todos Success',
props<{ todos: ITodo[] }>(),
);

export const fetchTodosError = createAction('[Todos] Fetch Todos Error');

export const updateTodo = createAction(
'[Todos] Update Todo',
props<{ todo: ITodo }>(),
);

export const updateTodoSuccess = createAction(
'[Todos] Update Todo Success',
props<{ todo: ITodo }>(),
);

export const updateTodoError = createAction(
'[Todos] Update Todo Error',
props<{ message: string; id: number }>(),
);

export const deleteTodo = createAction(
'[Todos] Delete Todo',
props<{
id: number;
}>(),
);

export const deleteTodoSuccess = createAction(
'[Todos] Delete Todo Success',
props<{
id: number;
}>(),
);
export const deleteTodoError = createAction(
'[Todos] Delete Todo Error',
props<{ message: string }>(),
);
Loading