├── src ├── styles.scss ├── assets │ └── .gitkeep ├── favicon.ico ├── app │ ├── 04-entities │ │ ├── todos.helpers.ts │ │ ├── todo.model.ts │ │ ├── ui │ │ │ ├── add-todo.component.ts │ │ │ └── todo-list.component.ts │ │ ├── todos.store.ts │ │ └── todos.component.ts │ ├── 03-rxjs-integration │ │ ├── user.model.ts │ │ ├── ui │ │ │ ├── user-list.component.ts │ │ │ └── search-box.component.ts │ │ ├── users.component.ts │ │ ├── users.service.ts │ │ └── users.store.ts │ ├── app.config.ts │ ├── 02-signal-store-feature │ │ ├── counter.store.ts │ │ ├── counter.feature.ts │ │ └── counter.component.ts │ ├── app.component.ts │ ├── app.routes.ts │ └── 01-signal-store │ │ ├── counter.component.ts │ │ └── counter.store.ts ├── main.ts └── index.html ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── tsconfig.app.json ├── .editorconfig ├── .gitignore ├── README.md ├── tsconfig.json ├── package.json └── angular.json /src/styles.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markostanimirovic/ngrx-signals-example/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/app/04-entities/todos.helpers.ts: -------------------------------------------------------------------------------- 1 | let id = 1; 2 | 3 | export function getNextId(): number { 4 | return id++; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/04-entities/todo.model.ts: -------------------------------------------------------------------------------- 1 | export type Todo = { 2 | id: number; 3 | text: string; 4 | completed: boolean; 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/03-rxjs-integration/user.model.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: number; 3 | firstName: string; 4 | lastName: string; 5 | }; 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | import { routes } from './app.routes'; 4 | 5 | export const appConfig: ApplicationConfig = { 6 | providers: [provideRouter(routes)], 7 | }; 8 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch((err) => 6 | console.error(err), 7 | ); 8 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app" 6 | }, 7 | "files": ["src/main.ts"], 8 | "include": ["src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgrxSignalsExample 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/app/02-signal-store-feature/counter.store.ts: -------------------------------------------------------------------------------- 1 | import { effect } from '@angular/core'; 2 | import { signalStore, withHooks } from '@ngrx/signals'; 3 | import { withCounter } from './counter.feature'; 4 | 5 | export const CounterStore = signalStore( 6 | { providedIn: 'root' }, 7 | withCounter(), 8 | withHooks({ 9 | onInit({ doubleCount }) { 10 | effect(() => { 11 | console.log('doubleCount changed', doubleCount()); 12 | }); 13 | }, 14 | }), 15 | ); 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { RouterLink, RouterOutlet } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | standalone: true, 7 | imports: [RouterOutlet, RouterLink], 8 | template: ` 9 | signalStore | 10 | signalStoreFeature | 11 | RxJS Integration | 12 | Entities 13 | 14 | 15 | `, 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | }) 18 | export class AppComponent {} 19 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { path: '', redirectTo: '01', pathMatch: 'full' }, 5 | { 6 | path: '01', 7 | loadComponent: () => import('./01-signal-store/counter.component'), 8 | }, 9 | { 10 | path: '02', 11 | loadComponent: () => import('./02-signal-store-feature/counter.component'), 12 | }, 13 | { 14 | path: '03', 15 | loadComponent: () => import('./03-rxjs-integration/users.component'), 16 | }, 17 | { 18 | path: '04', 19 | loadComponent: () => import('./04-entities/todos.component'), 20 | }, 21 | ]; 22 | -------------------------------------------------------------------------------- /src/app/04-entities/ui/add-todo.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | Output, 6 | } from '@angular/core'; 7 | 8 | @Component({ 9 | selector: 'app-add-todo', 10 | standalone: true, 11 | template: ` 12 | 18 | 19 | `, 20 | changeDetection: ChangeDetectionStrategy.OnPush, 21 | }) 22 | export class AddTodoComponent { 23 | @Output() add = new EventEmitter(); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/03-rxjs-integration/ui/user-list.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | import { User } from '../user.model'; 3 | 4 | @Component({ 5 | selector: 'app-user-list', 6 | standalone: true, 7 | template: ` 8 | @if (isLoading) { 9 | Loading... 10 | } 11 | 12 | 17 | `, 18 | changeDetection: ChangeDetectionStrategy.OnPush, 19 | }) 20 | export class UserListComponent { 21 | @Input() users: User[] = []; 22 | @Input() isLoading = false; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/02-signal-store-feature/counter.feature.ts: -------------------------------------------------------------------------------- 1 | import { computed } from '@angular/core'; 2 | import { 3 | patchState, 4 | signalStoreFeature, 5 | withComputed, 6 | withMethods, 7 | withState, 8 | } from '@ngrx/signals'; 9 | 10 | export function withCounter() { 11 | return signalStoreFeature( 12 | withState({ count: 0 }), 13 | withComputed(({ count }) => ({ 14 | doubleCount: computed(() => count() * 2), 15 | })), 16 | withMethods(({ count, ...store }) => ({ 17 | increment() { 18 | patchState(store, { count: count() + 1 }); 19 | }, 20 | decrement() { 21 | patchState(store, { count: count() - 1 }); 22 | }, 23 | })), 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/03-rxjs-integration/ui/search-box.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | Output, 7 | } from '@angular/core'; 8 | import { FormsModule } from '@angular/forms'; 9 | 10 | @Component({ 11 | selector: 'app-search-box', 12 | standalone: true, 13 | imports: [FormsModule], 14 | template: ` 15 | 20 | `, 21 | changeDetection: ChangeDetectionStrategy.OnPush, 22 | }) 23 | export class SearchBoxComponent { 24 | @Input() query = ''; 25 | @Output() queryChange = new EventEmitter(); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/02-signal-store-feature/counter.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; 2 | import { CounterStore } from './counter.store'; 3 | 4 | @Component({ 5 | selector: 'app-counter2', 6 | standalone: true, 7 | template: ` 8 |

Counter (signalStoreFeature)

9 | 10 |

Count: {{ store.count() }}

11 |

Double Count: {{ store.doubleCount() }}

12 | 13 | 14 | 15 | `, 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | }) 18 | export default class CounterComponent { 19 | readonly store = inject(CounterStore); 20 | } 21 | -------------------------------------------------------------------------------- /src/app/01-signal-store/counter.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; 2 | import { CounterStore } from './counter.store'; 3 | 4 | @Component({ 5 | selector: 'app-counter', 6 | standalone: true, 7 | template: ` 8 |

Counter (signalStore)

9 | 10 |

Count: {{ store.count() }}

11 |

Double Count: {{ store.doubleCount() }}

12 | 13 | 14 | 15 | `, 16 | providers: [CounterStore], 17 | changeDetection: ChangeDetectionStrategy.OnPush, 18 | }) 19 | export default class CounterComponent { 20 | readonly store = inject(CounterStore); 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NgrxSignalsExample 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.0-rc.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Further help 18 | 19 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 20 | -------------------------------------------------------------------------------- /src/app/03-rxjs-integration/users.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; 2 | import { SearchBoxComponent } from './ui/search-box.component'; 3 | import { UserListComponent } from './ui/user-list.component'; 4 | import { UsersStore } from './users.store'; 5 | 6 | @Component({ 7 | selector: 'app-users', 8 | standalone: true, 9 | imports: [SearchBoxComponent, UserListComponent], 10 | template: ` 11 |

Users (RxJS Integration)

12 | 13 | 17 | 18 | 19 | `, 20 | changeDetection: ChangeDetectionStrategy.OnPush, 21 | }) 22 | export default class UsersComponent { 23 | readonly store = inject(UsersStore); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/04-entities/todos.store.ts: -------------------------------------------------------------------------------- 1 | import { patchState, signalStore, withMethods } from '@ngrx/signals'; 2 | import { 3 | addEntity, 4 | removeEntity, 5 | updateEntity, 6 | withEntities, 7 | } from '@ngrx/signals/entities'; 8 | import { Todo } from './todo.model'; 9 | import { getNextId } from './todos.helpers'; 10 | 11 | export const TodosStore = signalStore( 12 | withEntities(), 13 | withMethods((store) => ({ 14 | addTodo(text: string) { 15 | const todo = { id: getNextId(), text, completed: false }; 16 | patchState(store, addEntity(todo)); 17 | }, 18 | removeTodo(id: number) { 19 | patchState(store, removeEntity(id)); 20 | }, 21 | toggleTodo(id: number) { 22 | const changes = (todo: Todo) => ({ completed: !todo.completed }); 23 | patchState(store, updateEntity({ id, changes })); 24 | }, 25 | })), 26 | ); 27 | -------------------------------------------------------------------------------- /src/app/04-entities/todos.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; 2 | import { TodosStore } from './todos.store'; 3 | import { AddTodoComponent } from './ui/add-todo.component'; 4 | import { TodoListComponent } from './ui/todo-list.component'; 5 | 6 | @Component({ 7 | selector: 'app-todos', 8 | standalone: true, 9 | imports: [AddTodoComponent, TodoListComponent], 10 | template: ` 11 |

Todos (Entities)

12 | 13 | 14 | 15 | 20 | `, 21 | providers: [TodosStore], 22 | changeDetection: ChangeDetectionStrategy.OnPush, 23 | }) 24 | export default class TodosComponent { 25 | readonly store = inject(TodosStore); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/03-rxjs-integration/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { delay, Observable, of } from 'rxjs'; 3 | import { User } from './user.model'; 4 | 5 | const usersMock: User[] = [ 6 | { id: 1, firstName: 'John', lastName: 'Smith' }, 7 | { id: 2, firstName: 'Jane', lastName: 'Doe' }, 8 | { id: 3, firstName: 'Bob', lastName: 'Brown' }, 9 | { id: 4, firstName: 'Mary', lastName: 'Jones' }, 10 | { id: 5, firstName: 'Kate', lastName: 'Parker' }, 11 | ]; 12 | 13 | @Injectable({ providedIn: 'root' }) 14 | export class UsersService { 15 | getAll(): Promise { 16 | return new Promise((resolve) => { 17 | setTimeout(() => resolve(usersMock), 1000); 18 | }); 19 | } 20 | 21 | getByQuery(query: string): Observable { 22 | const filteredUsers = usersMock.filter(({ firstName, lastName }) => 23 | `${firstName} ${lastName}`.toLowerCase().includes(query.toLowerCase()), 24 | ); 25 | 26 | return of(filteredUsers).pipe(delay(500)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": ["ES2022", "dom"] 23 | }, 24 | "angularCompilerOptions": { 25 | "enableI18nLegacyMessageIdFormat": false, 26 | "strictInjectionParameters": true, 27 | "strictInputAccessModifiers": true, 28 | "strictTemplates": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/01-signal-store/counter.store.ts: -------------------------------------------------------------------------------- 1 | import { computed } from '@angular/core'; 2 | import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; 3 | import { interval } from 'rxjs'; 4 | import { 5 | patchState, 6 | signalStore, 7 | withComputed, 8 | withHooks, 9 | withMethods, 10 | withState, 11 | } from '@ngrx/signals'; 12 | 13 | export const CounterStore = signalStore( 14 | withState({ count: 0 }), 15 | withComputed(({ count }) => ({ 16 | doubleCount: computed(() => count() * 2), 17 | })), 18 | withMethods(({ count, ...store }) => ({ 19 | increment() { 20 | patchState(store, { count: count() + 1 }); 21 | }, 22 | decrement() { 23 | patchState(store, { count: count() - 1 }); 24 | }, 25 | })), 26 | withHooks({ 27 | onInit({ increment }) { 28 | interval(2_000) 29 | .pipe(takeUntilDestroyed()) 30 | .subscribe(() => increment()); 31 | }, 32 | onDestroy({ count }) { 33 | console.log('count on destroy', count()); 34 | }, 35 | }), 36 | ); 37 | -------------------------------------------------------------------------------- /src/app/04-entities/ui/todo-list.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | Output, 7 | } from '@angular/core'; 8 | import { Todo } from '../todo.model'; 9 | 10 | @Component({ 11 | selector: 'app-todo-list', 12 | standalone: true, 13 | template: ` 14 |
    15 | @for (todo of todos; track todo.id) { 16 |
  • 17 | 18 | {{ todo.text }} 19 | 20 |
  • 21 | } @empty { 22 |

    Nothing to do!

    23 | } 24 |
25 | `, 26 | styles: ` 27 | .completed { 28 | text-decoration: line-through; 29 | } 30 | `, 31 | changeDetection: ChangeDetectionStrategy.OnPush, 32 | }) 33 | export class TodoListComponent { 34 | @Input() todos: Todo[] = []; 35 | @Output() toggle = new EventEmitter(); 36 | @Output() remove = new EventEmitter(); 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngrx-signals-example", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "format": "prettier --write ." 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^17.0.4", 14 | "@angular/common": "^17.0.4", 15 | "@angular/compiler": "^17.0.4", 16 | "@angular/core": "^17.0.4", 17 | "@angular/forms": "^17.0.4", 18 | "@angular/platform-browser": "^17.0.4", 19 | "@angular/platform-browser-dynamic": "^17.0.4", 20 | "@angular/router": "^17.0.4", 21 | "@ngrx/operators": "^17.0.1", 22 | "@ngrx/signals": "^17.0.1", 23 | "rxjs": "~7.8.1", 24 | "tslib": "^2.3.0", 25 | "zone.js": "~0.14.2" 26 | }, 27 | "devDependencies": { 28 | "@angular-devkit/build-angular": "^17.0.3", 29 | "@angular/cli": "^17.0.3", 30 | "@angular/compiler-cli": "^17.0.4", 31 | "prettier": "^3.1.0", 32 | "typescript": "~5.2.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/app/03-rxjs-integration/users.store.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { debounceTime, distinctUntilChanged, pipe, switchMap, tap } from 'rxjs'; 3 | import { 4 | patchState, 5 | signalStore, 6 | withHooks, 7 | withMethods, 8 | withState, 9 | } from '@ngrx/signals'; 10 | import { rxMethod } from '@ngrx/signals/rxjs-interop'; 11 | import { tapResponse } from '@ngrx/operators'; 12 | import { User } from './user.model'; 13 | import { UsersService } from './users.service'; 14 | 15 | type State = { users: User[]; isLoading: boolean; query: string }; 16 | 17 | const initialState: State = { 18 | users: [], 19 | isLoading: false, 20 | query: '', 21 | }; 22 | 23 | export const UsersStore = signalStore( 24 | { providedIn: 'root' }, 25 | withState(initialState), 26 | withMethods((store, usersService = inject(UsersService)) => ({ 27 | updateQuery(query: string) { 28 | patchState(store, { query }); 29 | }, 30 | async loadAll() { 31 | patchState(store, { isLoading: true }); 32 | const users = await usersService.getAll(); 33 | patchState(store, { users, isLoading: false }); 34 | }, 35 | loadByQuery: rxMethod( 36 | pipe( 37 | debounceTime(300), 38 | distinctUntilChanged(), 39 | tap(() => patchState(store, { isLoading: true })), 40 | switchMap((query) => 41 | usersService.getByQuery(query).pipe( 42 | tapResponse({ 43 | next: (users) => patchState(store, { users }), 44 | error: console.error, 45 | finalize: () => patchState(store, { isLoading: false }), 46 | }), 47 | ), 48 | ), 49 | ), 50 | ), 51 | })), 52 | withHooks({ 53 | onInit({ loadByQuery, query }) { 54 | loadByQuery(query); 55 | }, 56 | }), 57 | ); 58 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "cli": { 5 | "packageManager": "yarn" 6 | }, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "ngrx-signals-example": { 10 | "projectType": "application", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:application", 22 | "options": { 23 | "outputPath": "dist/ngrx-signals-example", 24 | "index": "src/index.html", 25 | "browser": "src/main.ts", 26 | "polyfills": ["zone.js"], 27 | "tsConfig": "tsconfig.app.json", 28 | "inlineStyleLanguage": "scss", 29 | "assets": ["src/favicon.ico", "src/assets"], 30 | "styles": ["src/styles.scss"], 31 | "scripts": [] 32 | }, 33 | "configurations": { 34 | "production": { 35 | "budgets": [ 36 | { 37 | "type": "initial", 38 | "maximumWarning": "500kb", 39 | "maximumError": "1mb" 40 | }, 41 | { 42 | "type": "anyComponentStyle", 43 | "maximumWarning": "2kb", 44 | "maximumError": "4kb" 45 | } 46 | ], 47 | "outputHashing": "all" 48 | }, 49 | "development": { 50 | "optimization": false, 51 | "extractLicenses": false, 52 | "sourceMap": true 53 | } 54 | }, 55 | "defaultConfiguration": "production" 56 | }, 57 | "serve": { 58 | "builder": "@angular-devkit/build-angular:dev-server", 59 | "configurations": { 60 | "production": { 61 | "buildTarget": "ngrx-signals-example:build:production" 62 | }, 63 | "development": { 64 | "buildTarget": "ngrx-signals-example:build:development" 65 | } 66 | }, 67 | "defaultConfiguration": "development" 68 | }, 69 | "extract-i18n": { 70 | "builder": "@angular-devkit/build-angular:extract-i18n", 71 | "options": { 72 | "buildTarget": "ngrx-signals-example:build" 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | --------------------------------------------------------------------------------