diff --git a/.vscode/settings.json b/.vscode/settings.json
index e6e7934..c08915c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,5 +7,10 @@
"**/.DS_Store": true,
"**/Thumbs.db": true
},
- "hide-files.files": []
+ "hide-files.files": [],
+ "workbench.colorCustomizations": {
+ "activityBar.background": "#0A350D",
+ "titleBar.activeBackground": "#0E4A12",
+ "titleBar.activeForeground": "#F2FCF2"
+ }
}
\ No newline at end of file
diff --git a/apps/demo/src/app/app.component.html b/apps/demo/src/app/app.component.html
index 09f0fcd..0cf3840 100644
--- a/apps/demo/src/app/app.component.html
+++ b/apps/demo/src/app/app.component.html
@@ -2,6 +2,7 @@
DevTools
+ getSignalValues
withRedux
withDataService (Simple)
withDataService (Dynamic)
diff --git a/apps/demo/src/app/lazy-routes.ts b/apps/demo/src/app/lazy-routes.ts
index f0f68a7..3b9c6dc 100644
--- a/apps/demo/src/app/lazy-routes.ts
+++ b/apps/demo/src/app/lazy-routes.ts
@@ -8,9 +8,11 @@ import { FlightEditDynamicComponent } from './flight-search-data-service-dynamic
import { TodoStorageSyncComponent } from './todo-storage-sync/todo-storage-sync.component';
import { provideFlightStore } from './flight-search-redux-connector/+state/redux';
import { FlightSearchReducConnectorComponent } from './flight-search-redux-connector/flight-search.component';
+import { TodoWithComputedComponent } from './todo-with-computed/todo-with-computed.component';
export const lazyRoutes: Route[] = [
{ path: 'todo', component: TodoComponent },
+ { path: 'todo-with-computed', component: TodoWithComputedComponent},
{ path: 'flight-search', component: FlightSearchComponent },
{
path: 'flight-search-data-service-simple',
diff --git a/apps/demo/src/app/todo-with-computed/todo-with-computed-store.ts b/apps/demo/src/app/todo-with-computed/todo-with-computed-store.ts
new file mode 100644
index 0000000..7035d20
--- /dev/null
+++ b/apps/demo/src/app/todo-with-computed/todo-with-computed-store.ts
@@ -0,0 +1,64 @@
+import { computed } from '@angular/core';
+import { signalStore, withComputed, withHooks, withMethods } from '@ngrx/signals';
+import {
+ EntityMap,
+ removeEntity,
+ setEntity,
+ updateEntity,
+ withEntities,
+} from '@ngrx/signals/entities';
+import { getSignalValues, updateState, withDevtools } from 'ngrx-toolkit';
+
+export interface Todo {
+ id: number;
+ name: string;
+ finished: boolean;
+ description?: string;
+ deadline?: Date;
+}
+
+export type AddTodo = Omit;
+
+export const TodoWithComputedStore = signalStore(
+ { providedIn: 'root' },
+ withDevtools('todo'),
+ withEntities(),
+ withComputed(store => ({
+ openItems: computed(() => openItems(getSignalValues(store)))
+ })),
+ withMethods((store) => {
+ let currentId = 0;
+ return {
+ add(todo: AddTodo) {
+ updateState(store, 'add todo', setEntity({ id: ++currentId, ...todo }));
+ },
+
+ remove(id: number) {
+ updateState(store, 'remove todo', removeEntity(id));
+ },
+
+ toggleFinished(id: number): void {
+ const todo = store.entityMap()[id];
+ updateState(
+ store,
+ 'toggle todo',
+ updateEntity({ id, changes: { finished: !todo.finished } })
+ );
+ },
+ };
+ }),
+ withHooks({
+ onInit: (store) => {
+ store.add({ name: 'Go for a Walk', finished: false });
+ store.add({ name: 'Sleep 8 hours once', finished: false });
+ store.add({ name: 'Clean the room', finished: true });
+ },
+ })
+);
+
+
+function openItems(state: {entities: Todo[], entityMap: EntityMap} ): string {
+ const openIds = state.entities.filter(todo => !todo.finished).map(todo => todo.id);
+ const strings = openIds.map(id => state.entityMap[id].name);
+ return strings.join(', ');
+}
\ No newline at end of file
diff --git a/apps/demo/src/app/todo-with-computed/todo-with-computed.component.html b/apps/demo/src/app/todo-with-computed/todo-with-computed.component.html
new file mode 100644
index 0000000..6061fc6
--- /dev/null
+++ b/apps/demo/src/app/todo-with-computed/todo-with-computed.component.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+ delete
+
+
+
+
+
+ Name
+ {{ element.name }}
+
+
+
+
+ Description
+ {{ element.description }}
+
+
+
+
+ Deadline
+ {{
+ element.deadline
+ }}
+
+
+
+
+
+
+
+
+State values without entityMap and ids:
+
+ {{entireState() | json}}
+
\ No newline at end of file
diff --git a/apps/demo/src/app/todo-with-computed/todo-with-computed.component.scss b/apps/demo/src/app/todo-with-computed/todo-with-computed.component.scss
new file mode 100644
index 0000000..3fdfeaf
--- /dev/null
+++ b/apps/demo/src/app/todo-with-computed/todo-with-computed.component.scss
@@ -0,0 +1,5 @@
+.actions{
+ display: flex;
+ align-items: center;
+
+}
diff --git a/apps/demo/src/app/todo-with-computed/todo-with-computed.component.ts b/apps/demo/src/app/todo-with-computed/todo-with-computed.component.ts
new file mode 100644
index 0000000..713ed09
--- /dev/null
+++ b/apps/demo/src/app/todo-with-computed/todo-with-computed.component.ts
@@ -0,0 +1,42 @@
+import { Component, computed, effect, inject } from '@angular/core';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatIconModule } from '@angular/material/icon';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { Todo } from '../todo-store';
+import { CategoryStore } from '../category.store';
+import { SelectionModel } from '@angular/cdk/collections';
+import { TodoWithComputedStore } from './todo-with-computed-store';
+import { getSignalValues } from 'ngrx-toolkit';
+import { CommonModule } from '@angular/common';
+
+@Component({
+ selector: 'demo-todo-with-computed',
+ templateUrl: 'todo-with-computed.component.html',
+ styleUrl: 'todo-with-computed.component.scss',
+ standalone: true,
+ imports: [MatCheckboxModule, MatIconModule, MatTableModule, CommonModule],
+})
+export class TodoWithComputedComponent {
+ todoStore = inject(TodoWithComputedStore);
+ categoryStore = inject(CategoryStore);
+
+ displayedColumns: string[] = ['finished', 'name', 'description', 'deadline'];
+ dataSource = new MatTableDataSource([]);
+ selection = new SelectionModel(true, []);
+
+ entireState = computed(() => getSignalValues(this.todoStore, 'entityMap', 'ids'));
+
+ constructor() {
+ effect(() => {
+ this.dataSource.data = this.todoStore.entities();
+ });
+ }
+
+ checkboxLabel(todo: Todo) {
+ this.todoStore.toggleFinished(todo.id);
+ }
+
+ removeTodo(todo: Todo) {
+ this.todoStore.remove(todo.id);
+ }
+}
diff --git a/libs/ngrx-toolkit/src/index.ts b/libs/ngrx-toolkit/src/index.ts
index d1a0f18..79b7609 100644
--- a/libs/ngrx-toolkit/src/index.ts
+++ b/libs/ngrx-toolkit/src/index.ts
@@ -12,3 +12,4 @@ export * from './lib/with-data-service';
export { withStorageSync, SyncConfig } from './lib/with-storage-sync';
export * from './lib/redux-connector';
export * from './lib/redux-connector/rxjs-interop';
+export * from './lib/shared/get-signal-values';
diff --git a/libs/ngrx-toolkit/src/lib/shared/get-signal-values.ts b/libs/ngrx-toolkit/src/lib/shared/get-signal-values.ts
new file mode 100644
index 0000000..ca02c87
--- /dev/null
+++ b/libs/ngrx-toolkit/src/lib/shared/get-signal-values.ts
@@ -0,0 +1,45 @@
+import { Signal } from '@angular/core';
+import { SIGNAL } from '@angular/core/primitives/signals';
+
+type Rec = Record;
+
+type SignalPropertyNames = {
+ [K in keyof T]: T[K] extends Signal ? K : never;
+}[keyof T];
+
+type SignalProperties = Pick<
+ T,
+ Exclude, Without>
+>;
+
+type SignalPropertyValues = {
+ [K in keyof SignalProperties<
+ T,
+ Without
+ >]: SignalProperties[K] extends Signal
+ ? ReturnType[K]>
+ : never;
+};
+
+export function getSignalValues(
+ signals: T,
+ ...exclude: Without[]
+): SignalPropertyValues {
+ const result: Partial> = {};
+ const withouts = new Set(exclude);
+
+ const entries = Object.entries(signals)
+ .filter(
+ ([_, value]) =>
+ typeof value === 'function' && hasOwnSymbol(value, SIGNAL)
+ )
+ .filter(([key]) => !withouts.has(key as Without))
+ .map(([key, value]) => [key, value()]);
+
+ const reduced = Object.fromEntries(entries) as SignalPropertyValues;
+ return reduced;
+}
+
+export function hasOwnSymbol(obj: any, symbol: symbol) {
+ return Object.getOwnPropertySymbols(obj).includes(symbol);
+}