import { computed, inject } from '@angular/core'; import { addEntity, removeEntities, setAllEntities, updateEntity, withEntities, } from '@ngrx/signals/entities'; import { signalStore, withComputed, withHooks, withMethods, withState, } from '@ngrx/signals'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { setError, setLoaded, setLoading, updateState, withCallState, withDevtools, withLocalStorage, withStorageSync, } from '@angular-architects/ngrx-toolkit'; import { EMPTY, pipe } from 'rxjs'; import { catchError, switchMap, tap } from 'rxjs/operators'; import { Task, TaskFilter, TaskPriority } from './task.model'; import { TasksApiService } from './tasks-api.service'; interface TasksState { filter: TaskFilter; draftPriority: TaskPriority; searchTerm: string; lastSyncedAt: string | null; } const initialState: TasksState = { filter: 'all', draftPriority: 'medium', searchTerm: '', lastSyncedAt: null, }; function matchesFilter(task: Task, filter: TaskFilter): boolean { switch (filter) { case 'active': return !task.completed; case 'completed': return task.completed; default: return true; } } export const TasksStore = signalStore( { providedIn: 'root' }, withState(initialState), withEntities(), withCallState(), withDevtools('tasks-store'), withStorageSync( { key: 'ngrx-playground.tasks', select: (state) => ({ ids: state.ids, entityMap: state.entityMap, filter: state.filter, draftPriority: state.draftPriority, searchTerm: state.searchTerm, lastSyncedAt: state.lastSyncedAt, }), }, withLocalStorage(), ), withComputed((store) => ({ filteredTasks: computed(() => { const normalizedSearch = store.searchTerm().trim().toLowerCase(); return store.entities().filter((task) => { const matchesText = normalizedSearch.length === 0 || task.title.toLowerCase().includes(normalizedSearch); return matchesFilter(task, store.filter()) && matchesText; }); }), totalCount: computed(() => store.entities().length), activeCount: computed(() => store.entities().filter((task) => !task.completed).length), completedCount: computed(() => store.entities().filter((task) => task.completed).length), hasCompletedTasks: computed(() => store.entities().some((task) => task.completed)), emptyStateMessage: computed(() => { if (store.loading()) { return 'Loading tasks...'; } if (store.error()) { return 'The task list could not be refreshed.'; } if (store.entities().length === 0) { return 'No tasks saved yet. Create the first one.'; } return 'No tasks match the current filters.'; }), lastSyncedLabel: computed(() => { const value = store.lastSyncedAt(); return value ? new Date(value).toLocaleTimeString('en-US') : 'Never'; }), })), methodsStore(), withHooks({ onInit(store) { if (store.entities().length === 0) { store.loadTasks(); } }, }), ); function methodsStore() { return withMethods((store, api = inject(TasksApiService)) => ({ loadTasks: rxMethod( pipe( tap(() => updateState(store, 'tasks load started', setLoading())), switchMap(() => api.getTasks().pipe( tap((tasks) => updateState( store, 'tasks load succeeded', setAllEntities(tasks), { lastSyncedAt: new Date().toISOString() }, setLoaded(), ), ), catchError((error: unknown) => { updateState(store, 'tasks load failed', setError(error)); return EMPTY; }), ), ), ), ), createTask: rxMethod<{ title: string; priority: TaskPriority }>( pipe( tap(() => updateState(store, 'task create started', setLoading())), switchMap(({ title, priority }) => api.createTask(title, priority).pipe( tap((task) => updateState(store, 'task create succeeded', addEntity(task), { lastSyncedAt: task.updatedAt }, setLoaded()), ), catchError((error: unknown) => { updateState(store, 'task create failed', setError(error)); return EMPTY; }), ), ), ), ), toggleTask: rxMethod( pipe( tap(() => updateState(store, 'task toggle started', setLoading())), switchMap((taskId) => api.toggleTask(taskId).pipe( tap((task) => updateState( store, 'task toggle succeeded', updateEntity({ id: task.id, changes: task }), { lastSyncedAt: task.updatedAt }, setLoaded(), ), ), catchError((error: unknown) => { updateState(store, 'task toggle failed', setError(error)); return EMPTY; }), ), ), ), ), archiveCompleted: rxMethod( pipe( tap(() => updateState(store, 'completed tasks archive started', setLoading())), switchMap(() => api.archiveCompleted().pipe( tap((archivedIds) => updateState( store, 'completed tasks archive succeeded', removeEntities(archivedIds), { lastSyncedAt: new Date().toISOString() }, setLoaded(), ), ), catchError((error: unknown) => { updateState(store, 'completed tasks archive failed', setError(error)); return EMPTY; }), ), ), ), ), setFilter(filter: TaskFilter): void { updateState(store, 'task filter changed', { filter }); }, setDraftPriority(priority: TaskPriority): void { updateState(store, 'task priority draft changed', { draftPriority: priority }); }, setSearchTerm(searchTerm: string): void { updateState(store, 'task search changed', { searchTerm }); }, })); }