Skip to content

mkv-mkverma/NgRx-Todo-List

Repository files navigation

/* 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: `

Todo List

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

About

Created with StackBlitz ⚡️

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published