diff --git a/package-lock.json b/package-lock.json
index 095190f..428af3c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "laboratorio-mydayapp-angular",
+ "name": "todo-app-ngrx",
"version": "0.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "name": "laboratorio-mydayapp-angular",
+ "name": "todo-app-ngrx",
"version": "0.0.0",
"dependencies": {
"@angular/animations": "18.2.5",
@@ -16,6 +16,7 @@
"@angular/platform-browser": "18.2.5",
"@angular/platform-browser-dynamic": "18.2.5",
"@angular/router": "18.2.5",
+ "@ngrx/signals": "^18.0.2",
"rxjs": "7.5.0",
"tslib": "2.3.0",
"zone.js": "0.14.10"
@@ -4076,6 +4077,24 @@
"win32"
]
},
+ "node_modules/@ngrx/signals": {
+ "version": "18.0.2",
+ "resolved": "https://registry.npmjs.org/@ngrx/signals/-/signals-18.0.2.tgz",
+ "integrity": "sha512-FXmcY2cmkbhZtg9k8Ntq69SyelGmmb6fWtdButH4T8GGFH0o3f1FZTR829j4ynphy8SzuDhD/pzrnpWcV481oQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/core": "^18.0.0",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ },
+ "peerDependenciesMeta": {
+ "rxjs": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@ngtools/webpack": {
"version": "18.2.5",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.5.tgz",
@@ -17986,6 +18005,14 @@
"dev": true,
"optional": true
},
+ "@ngrx/signals": {
+ "version": "18.0.2",
+ "resolved": "https://registry.npmjs.org/@ngrx/signals/-/signals-18.0.2.tgz",
+ "integrity": "sha512-FXmcY2cmkbhZtg9k8Ntq69SyelGmmb6fWtdButH4T8GGFH0o3f1FZTR829j4ynphy8SzuDhD/pzrnpWcV481oQ==",
+ "requires": {
+ "tslib": "^2.3.0"
+ }
+ },
"@ngtools/webpack": {
"version": "18.2.5",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.5.tgz",
diff --git a/package.json b/package.json
index f4df9a7..cfd38b4 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"@angular/platform-browser": "18.2.5",
"@angular/platform-browser-dynamic": "18.2.5",
"@angular/router": "18.2.5",
+ "@ngrx/signals": "^18.0.2",
"rxjs": "7.5.0",
"tslib": "2.3.0",
"zone.js": "0.14.10"
@@ -49,4 +50,4 @@
"karma-jasmine-html-reporter": "1.7.0",
"typescript": "5.4.5"
}
-}
+}
\ No newline at end of file
diff --git a/src/app/components/clear-btn/clear-btn.component.html b/src/app/components/clear-btn/clear-btn.component.html
index fab5483..1efa51e 100644
--- a/src/app/components/clear-btn/clear-btn.component.html
+++ b/src/app/components/clear-btn/clear-btn.component.html
@@ -1,11 +1,13 @@
-@if (completedTodos$ | async; as todos) {
- @if (todos.length > 0) {
+@let completedTodos = store.completedTodos();
+
+
+ @if (completedTodos.length > 0) {
}
-}
+
diff --git a/src/app/components/clear-btn/clear-btn.component.ts b/src/app/components/clear-btn/clear-btn.component.ts
index e5950f1..f1f3e27 100644
--- a/src/app/components/clear-btn/clear-btn.component.ts
+++ b/src/app/components/clear-btn/clear-btn.component.ts
@@ -1,23 +1,13 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
-import { AsyncPipe } from '@angular/common';
-import { TodoService } from '@services/todo.service';
+import { TodosStore } from '@services/todos.store';
@Component({
standalone: true,
- imports: [AsyncPipe],
selector: 'app-clear-btn',
templateUrl: './clear-btn.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClearBtnComponent {
- private todoService = inject(TodoService);
-
-
- completedTodos$ = this.todoService.getCompletedTodos();
-
- clear() {
- this.todoService.clearCompleted();
- }
-
+ readonly store = inject(TodosStore);
}
diff --git a/src/app/components/counter/counter.component.html b/src/app/components/counter/counter.component.html
index b220132..8d76cb0 100644
--- a/src/app/components/counter/counter.component.html
+++ b/src/app/components/counter/counter.component.html
@@ -1,11 +1,12 @@
-@if (pendingTodos$ | async; as pendingTodos) {
-
- {{ pendingTodos.length }}
- @if (pendingTodos.length === 1) {
- item
- } @else {
- items
- }
- left
-
-}
+@let pendingTodos = store.pendingTodos();
+
+
+ {{ pendingTodos.length }}
+ @if (pendingTodos.length === 1) {
+ item
+ } @else {
+ items
+ }
+ left
+
+
diff --git a/src/app/components/counter/counter.component.ts b/src/app/components/counter/counter.component.ts
index 0f28e0b..8a14d42 100644
--- a/src/app/components/counter/counter.component.ts
+++ b/src/app/components/counter/counter.component.ts
@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
-import { TodoService } from '@services/todo.service';
+import { TodosStore } from '@services/todos.store';
@Component({
standalone: true,
@@ -11,9 +11,5 @@ import { TodoService } from '@services/todo.service';
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent{
- private todoService = inject(TodoService);
-
-
- pendingTodos$ = this.todoService.getPendingTodos();
-
+ readonly store = inject(TodosStore);
}
diff --git a/src/app/components/footer/footer.component.html b/src/app/components/footer/footer.component.html
index ffd9a24..98202bb 100644
--- a/src/app/components/footer/footer.component.html
+++ b/src/app/components/footer/footer.component.html
@@ -1,25 +1,26 @@
-@if (todos$ | async; as todos) {
- @if (todos.length > 0) {
-
}
diff --git a/src/app/components/footer/footer.component.ts b/src/app/components/footer/footer.component.ts
index 07997de..8ed57a1 100644
--- a/src/app/components/footer/footer.component.ts
+++ b/src/app/components/footer/footer.component.ts
@@ -1,23 +1,17 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
-import { AsyncPipe } from '@angular/common';
-import { TodoService } from '@services/todo.service';
+import { TodosStore } from '@services/todos.store';
import { CounterComponent } from '@components/counter/counter.component';
import { ClearBtnComponent } from '@components/clear-btn/clear-btn.component';
@Component({
standalone: true,
- imports: [AsyncPipe, CounterComponent, ClearBtnComponent, RouterLink],
+ imports: [CounterComponent, ClearBtnComponent, RouterLink],
selector: 'app-footer',
templateUrl: './footer.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FooterComponent {
- private todoService = inject(TodoService);
-
-
- todos$ = this.todoService.getTodos();
- filter$ = this.todoService.getFilter();
-
+ readonly store = inject(TodosStore);
}
diff --git a/src/app/components/header/header.component.ts b/src/app/components/header/header.component.ts
index dd3381b..fd4e648 100644
--- a/src/app/components/header/header.component.ts
+++ b/src/app/components/header/header.component.ts
@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
-import { TodoService } from '@services/todo.service';
+import { TodosStore } from '@services/todos.store';
@Component({
standalone: true,
@@ -11,14 +11,14 @@ import { TodoService } from '@services/todo.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderComponent {
- private todoService = inject(TodoService);
+ readonly store = inject(TodosStore);
input = new FormControl('', { nonNullable: true });
addTodo() {
const title = this.input.value.trim();
if (title !== '') {
- this.todoService.add(title);
+ this.store.add(title);
this.input.setValue('');
}
}
diff --git a/src/app/components/todo/todo.component.ts b/src/app/components/todo/todo.component.ts
index 28a3b30..b965ff3 100644
--- a/src/app/components/todo/todo.component.ts
+++ b/src/app/components/todo/todo.component.ts
@@ -4,7 +4,7 @@ import { NgClass } from '@angular/common';
import { Todo } from '@models/todo.model';
-import { TodoService } from '@services/todo.service';
+import { TodosStore } from '@services/todos.store';
@Component({
standalone: true,
@@ -14,7 +14,7 @@ import { TodoService } from '@services/todo.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TodoComponent {
- private todoService = inject(TodoService);
+ readonly store = inject(TodosStore);
private cdRef = inject(ChangeDetectorRef);
_todo!: Todo;
@@ -28,18 +28,18 @@ export class TodoComponent {
@ViewChild('inputElement') inputElement!: ElementRef;
toggle() {
- this.todoService.toggle(this._todo.id);
+ this.store.toggle(this._todo.id);
}
update() {
const title = this.input.value.trim();
if (title !== '') {
- this.todoService.update(this._todo.id, { title });
+ this.store.update(this._todo.id, { title });
}
}
remove() {
- this.todoService.remove(this._todo.id);
+ this.store.remove(this._todo.id);
}
escape() {
diff --git a/src/app/components/todos/todos.component.html b/src/app/components/todos/todos.component.html
index b9e7e46..62a193f 100644
--- a/src/app/components/todos/todos.component.html
+++ b/src/app/components/todos/todos.component.html
@@ -1,11 +1,11 @@
-@if (todos$ | async; as todos) {
- @if (todos.length > 0) {
-
-
- @for (todo of todos; track todo) {
-
- }
-
-
- }
+@let todos = store.visibleTodos();
+@if (todos.length > 0) {
+
+
+ @for (todo of todos; track todo.id) {
+
+ }
+
+
}
+
diff --git a/src/app/components/todos/todos.component.ts b/src/app/components/todos/todos.component.ts
index 34adf94..645abaa 100644
--- a/src/app/components/todos/todos.component.ts
+++ b/src/app/components/todos/todos.component.ts
@@ -1,32 +1,29 @@
import { Component, OnInit, ChangeDetectionStrategy, inject } from '@angular/core';
-import { AsyncPipe } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
import { Filter } from '@models/filter.model';
import { TodoComponent } from '@components/todo/todo.component';
-import { TodoService } from '@services/todo.service';
+import { TodosStore } from '@services/todos.store';
@Component({
standalone: true,
- imports: [TodoComponent, AsyncPipe],
+ imports: [TodoComponent],
selector: 'app-todos',
templateUrl: './todos.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TodosComponent implements OnInit {
- private todoService = inject(TodoService);
+ readonly store = inject(TodosStore);
private route = inject(ActivatedRoute);
- todos$ = this.todoService.getTodosByFilter();
-
constructor() {
this.route.paramMap.subscribe((params) => {
const filter = params.get('filter') as Filter;
- this.todoService.changeFilter(filter || 'all');
+ this.store.changeFilter(filter || 'all');
});
}
ngOnInit(): void {
- this.todoService.readStorage();
+ // this.store.readStorage();
}
}
diff --git a/src/app/pages/home/home.component.html b/src/app/pages/home/home.component.html
index ccb3962..abcfd12 100644
--- a/src/app/pages/home/home.component.html
+++ b/src/app/pages/home/home.component.html
@@ -1,7 +1,7 @@
diff --git a/src/app/services/todos.store.ts b/src/app/services/todos.store.ts
new file mode 100644
index 0000000..dd45926
--- /dev/null
+++ b/src/app/services/todos.store.ts
@@ -0,0 +1,87 @@
+import { signalStore, withState, withMethods, patchState, withComputed } from '@ngrx/signals';
+
+import { Todo, UpdateTodoDto } from '@models/todo.model';
+import { Filter } from '@models/filter.model';
+import { computed } from '@angular/core';
+
+type TodosState = {
+ todos: Todo[];
+ filter: Filter;
+};
+
+const initialState: TodosState = {
+ todos: [],
+ filter: 'all',
+};
+
+export const TodosStore = signalStore(
+ { providedIn: 'root' },
+ withState(initialState),
+ withComputed((state) => ({
+ visibleTodos: computed(() => {
+ const todos = state.todos();
+ const filter = state.filter();
+
+ if (filter === 'pending') {
+ return todos.filter((todo) => !todo.completed);
+ }
+ if (filter === 'completed') {
+ return todos.filter((todo) => todo.completed);
+ }
+ return todos;
+ }),
+ pendingTodos: computed(() => {
+ return state.todos().filter((todo) => !todo.completed);
+ }),
+ completedTodos: computed(() => {
+ return state.todos().filter((todo) => todo.completed);
+ }),
+ })),
+ withMethods((store) => ({
+ add(title: string): void {
+ const newTodo = {
+ id: 'id_' + Date.now(),
+ title,
+ completed: false,
+ };
+ const todos = store.todos();
+ patchState(store, {
+ todos: [...todos, newTodo],
+ });
+ },
+ remove(id: string): void {
+ const todos = store.todos();
+ patchState(store, {
+ todos: todos.filter((todo) => todo.id !== id),
+ });
+ },
+ toggle(id: string): void {
+ patchState(store, (state) => ({
+ todos: state.todos.map((todo) =>
+ todo.id === id ? { ...todo, completed: !todo.completed } : todo
+ ),
+ })) ;
+ },
+ update(id: string, dto: UpdateTodoDto): void {
+ patchState(store, (state) => ({
+ todos: state.todos.map((todo) => {
+ if (todo.id === id) {
+ return {
+ ...todo,
+ ...dto,
+ };
+ }
+ return todo;
+ }),
+ })) ;
+ },
+ changeFilter(change: Filter) {
+ patchState(store, { filter: change });
+ },
+ clearCompleted(): void {
+ patchState(store, (state) => ({
+ todos: state.todos.filter((todo) => !todo.completed),
+ })) ;
+ },
+ }))
+);