/* Angular 18 + NgRx Full CRUD Example for Todo with UI
npm i @ngrx/store @ngrx/effects @ngrx/store-devtools
*/
// 1. Models (models/todo.model.ts) export interface Todo { id: number; title: string; completed: boolean; }
// 2. Actions (store/actions/todo.actions.ts) import { createAction, props } from '@ngrx/store'; import { Todo } from '../../models/todo.model';
export const loadTodos = createAction('[Todo] Load Todos'); export const loadTodosSuccess = createAction('[Todo] Load Success', props<{ todos: Todo[] }>()); export const addTodo = createAction('[Todo] Add Todo', props<{ todo: Todo }>()); export const updateTodo = createAction('[Todo] Update Todo', props<{ todo: Todo }>()); export const deleteTodo = createAction('[Todo] Delete Todo', props<{ id: number }>());
// 3. Reducer (store/reducers/todo.reducer.ts) import { createReducer, on } from '@ngrx/store'; import * as TodoActions from '../actions/todo.actions'; import { Todo } from '../../models/todo.model';
export interface TodoState { todos: Todo[]; }
export const initialState: TodoState = { todos: [] };
export const todoReducer = createReducer( initialState, on(TodoActions.loadTodosSuccess, (state, { todos }) => ({ ...state, todos })), on(TodoActions.addTodo, (state, { todo }) => ({ ...state, todos: [...state.todos, todo] })), on(TodoActions.updateTodo, (state, { todo }) => ({ ...state, todos: state.todos.map(t => t.id === todo.id ? todo : t) })), on(TodoActions.deleteTodo, (state, { id }) => ({ ...state, todos: state.todos.filter(t => t.id !== id) })) );
// 4. Selectors (store/selectors/todo.selectors.ts) import { createFeatureSelector, createSelector } from '@ngrx/store'; import { TodoState } from '../reducers/todo.reducer';
export const selectTodoState = createFeatureSelector('todos');
export const selectAllTodos = createSelector( selectTodoState, (state) => state.todos );
// 5. Effects (store/effects/todo.effects.ts) import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { TodoService } from '../../services/todo.service'; import * as TodoActions from '../actions/todo.actions'; import { map, mergeMap } from 'rxjs/operators';
@Injectable() export class TodoEffects { loadTodos$ = createEffect(() => this.actions$.pipe( ofType(TodoActions.loadTodos), mergeMap(() => this.todoService.getTodos() .pipe(map(todos => TodoActions.loadTodosSuccess({ todos })))) ) );
constructor(private actions$: Actions, private todoService: TodoService) {} }
// 6. Service (services/todo.service.ts) import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, of } from 'rxjs'; import { Todo } from '../models/todo.model';
@Injectable({ providedIn: 'root' }) export class TodoService { private todos: Todo[] = [ { id: 1, title: 'Learn NgRx', completed: false }, { id: 2, title: 'Write Code', completed: false } ];
constructor(private http: HttpClient) {}
getTodos(): Observable<Todo[]> { return of(this.todos); } }
// 7. Component (todo.component.ts) import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import * as TodoActions from './store/actions/todo.actions'; import { Observable } from 'rxjs'; import { Todo } from './models/todo.model'; import { selectAllTodos } from './store/selectors/todo.selectors';
@Component({ selector: 'app-todo', template: `
<form (submit)="onAdd()" class="mb-4 flex gap-2">
<input [(ngModel)]="newTitle" name="title" placeholder="New Todo" class="p-2 border rounded w-full" required />
<button type="submit" class="bg-blue-500 text-white px-4 rounded">Add</button>
</form>
<div *ngFor="let todo of todos$ | async" class="flex items-center justify-between p-2 border-b">
<div>
<input type="checkbox" [(ngModel)]="todo.completed" (change)="onUpdate(todo)" />
<span [class.line-through]="todo.completed" class="ml-2">{{ todo.title }}</span>
</div>
<button (click)="onDelete(todo.id)" class="text-red-500">Delete</button>
</div>
</div>
, styles: [
.line-through {
text-decoration: line-through;
}
`]
})
export class TodoComponent implements OnInit {
todos$: Observable<Todo[]> = this.store.select(selectAllTodos);
newTitle = '';
constructor(private store: Store) {}
ngOnInit(): void { this.store.dispatch(TodoActions.loadTodos()); }
onAdd(): void { if (!this.newTitle.trim()) return; const newTodo: Todo = { id: Math.floor(Math.random() * 10000), title: this.newTitle.trim(), completed: false }; this.store.dispatch(TodoActions.addTodo({ todo: newTodo })); this.newTitle = ''; }
onUpdate(todo: Todo): void { this.store.dispatch(TodoActions.updateTodo({ todo })); }
onDelete(id: number): void { this.store.dispatch(TodoActions.deleteTodo({ id })); } }