Skip to content

Commit

Permalink
RFC(Signals): Add selectable entity as default behaviour
Browse files Browse the repository at this point in the history
- Updated withEntities to add selectedId and selectedEntity (even for named collections);
- Created selectEntity helper to select an entity given its ID.
- Created clearSelectedEntity helper to remove the currently selected entity without caring about its ID.
- Added Unit Tests
- Added documentation

Issue reference: #4717
  • Loading branch information
DJREMiX6 committed Mar 8, 2025
1 parent a23a0a1 commit aef2533
Show file tree
Hide file tree
Showing 11 changed files with 329 additions and 74 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { patchState, signalStore } from '@ngrx/signals';
import {
addEntities,
withEntities,
selectEntity,
clearSelectedEntity,
} from '../../src';
import { User, user1, user2, user3 } from '../mocks';

describe('selectEntity', () => {
it('should clear the selectedEntity and selectedEntityId if an entity is selected and exists in the state', () => {
const Store = signalStore({ protectedState: false }, withEntities<User>());
const store = new Store();

patchState(store, addEntities([user1, user2, user3]));
patchState(store, selectEntity(user1.id));

expect(store.selectedId()).toBe(user1.id);
expect(store.selectedEntity()).toBe(user1);

patchState(store, clearSelectedEntity());

expect(store.selectedId()).toBe(null);
expect(store.selectedEntity()).toBe(null);
});

it('should clear the selectedEntity and selectedEntityId if an entity is selected and it does not exists in the state', () => {
const Store = signalStore({ protectedState: false }, withEntities<User>());
const store = new Store();

patchState(store, addEntities([user1, user2]));
patchState(store, selectEntity(user3.id));

expect(store.selectedId()).toBe(user3.id);
expect(store.selectedEntity()).toBe(null);

patchState(store, clearSelectedEntity());

expect(store.selectedId()).toBe(null);
expect(store.selectedEntity()).toBe(null);
});

it('should not change the state if an entity is not selected', () => {
const Store = signalStore({ protectedState: false }, withEntities<User>());
const store = new Store();

patchState(store, addEntities([user1, user2, user3]));

expect(store.selectedId()).toBe(null);
expect(store.selectedEntity()).toBe(null);

patchState(store, clearSelectedEntity());

expect(store.selectedId()).toBe(null);
expect(store.selectedEntity()).toBe(null);
});
});
49 changes: 49 additions & 0 deletions modules/signals/entities/spec/updaters/select-entity.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { patchState, signalStore, type } from '@ngrx/signals';
import {
addEntities,
addEntity,
withEntities,
selectEntity,
setEntities,
} from '../../src';
import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks';

describe('selectEntity', () => {
it('should select an entity and return it if exists', () => {
const Store = signalStore({ protectedState: false }, withEntities<User>());
const store = new Store();

patchState(store, addEntities([user1, user2, user3]));
patchState(store, selectEntity(user1.id));

expect(store.selectedId()).toBe(user1.id);
expect(store.selectedEntity()).toBe(user1);
});

it('should select an entity and return null if it does not exists', () => {
const Store = signalStore({ protectedState: false }, withEntities<User>());
const store = new Store();

patchState(store, addEntities([user1, user2]));
patchState(store, selectEntity(user3.id));

expect(store.selectedId()).toBe(user3.id);
expect(store.selectedEntity()).toBe(null);
});

it('should return null if the selected entity does not exist and return the entity as soon as it is added to the state', () => {
const Store = signalStore({ protectedState: false }, withEntities<User>());
const store = new Store();

patchState(store, addEntities([user1, user2]));
patchState(store, selectEntity(user3.id));

expect(store.selectedId()).toBe(user3.id);
expect(store.selectedEntity()).toBe(null);

patchState(store, addEntity(user3));

expect(store.selectedId()).toBe(user3.id);
expect(store.selectedEntity()).toBe(user3);
});
});
83 changes: 58 additions & 25 deletions modules/signals/entities/spec/with-entities.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,41 @@ import { Todo, todo2, todo3, User, user1, user2 } from './mocks';
import { selectTodoId } from './helpers';

describe('withEntities', () => {
it('adds entity feature to the store', () => {
const Store = signalStore(
withEntities<User>(),
withMethods((store) => ({
addUsers(): void {
patchState(store, addEntities([user1, user2]));
},
}))
);
const store = new Store();

expect(isSignal(store.entityMap)).toBe(true);
expect(store.entityMap()).toEqual({});

expect(isSignal(store.ids)).toBe(true);
expect(store.ids()).toEqual([]);

expect(isSignal(store.entities)).toBe(true);
expect(store.entities()).toEqual([]);

store.addUsers();

expect(store.entityMap()).toEqual({ 1: user1, 2: user2 });
expect(store.ids()).toEqual([1, 2]);
expect(store.entities()).toEqual([user1, user2]);
describe('signle entity feature', () => {
it('adds entity feature to the store', () => {
const Store = signalStore(
withEntities<User>(),
withMethods((store) => ({
addUsers(): void {
patchState(store, addEntities([user1, user2]));
},
}))
);
const store = new Store();

expect(isSignal(store.entityMap)).toBe(true);
expect(store.entityMap()).toEqual({});

expect(isSignal(store.ids)).toBe(true);
expect(store.ids()).toEqual([]);

expect(isSignal(store.entities)).toBe(true);
expect(store.entities()).toEqual([]);

expect(isSignal(store.selectedId)).toBe(true);
expect(store.selectedId()).toEqual(null);

expect(isSignal(store.selectedEntity)).toBe(true);
expect(store.selectedEntity()).toEqual(null);

store.addUsers();

expect(store.entityMap()).toEqual({ 1: user1, 2: user2 });
expect(store.ids()).toEqual([1, 2]);
expect(store.entities()).toEqual([user1, user2]);
expect(store.selectedId()).toEqual(null);
expect(store.selectedEntity()).toEqual(null);
});
});

it('adds named entity feature to the store', () => {
Expand All @@ -55,11 +65,19 @@ describe('withEntities', () => {
expect(isSignal(store.userEntities)).toBe(true);
expect(store.userEntities()).toEqual([]);

expect(isSignal(store.userSelectedId)).toBe(true);
expect(store.userSelectedId()).toEqual(null);

expect(isSignal(store.userSelectedEntity)).toBe(true);
expect(store.userSelectedEntity()).toEqual(null);

store.addUsers();

expect(store.userEntityMap()).toEqual({ 2: user2, 1: user1 });
expect(store.userIds()).toEqual([2, 1]);
expect(store.userEntities()).toEqual([user2, user1]);
expect(store.userSelectedId()).toEqual(null);
expect(store.userSelectedEntity()).toEqual(null);
});

it('combines multiple entity features', () => {
Expand Down Expand Up @@ -99,13 +117,28 @@ describe('withEntities', () => {
expect(isSignal(store.todoEntities)).toBe(true);
expect(store.todoEntities()).toEqual([]);

expect(isSignal(store.selectedId)).toBe(true);
expect(store.selectedId()).toEqual(null);
expect(isSignal(store.todoSelectedId)).toBe(true);
expect(store.todoSelectedId()).toEqual(null);

expect(isSignal(store.selectedEntity)).toBe(true);
expect(store.selectedEntity()).toEqual(null);
expect(isSignal(store.todoSelectedEntity)).toBe(true);
expect(store.todoSelectedEntity()).toEqual(null);

store.addEntities();

expect(store.entityMap()).toEqual({ 2: user2, 1: user1 });
expect(store.ids()).toEqual([2, 1]);
expect(store.entities()).toEqual([user2, user1]);
expect(store.selectedId()).toEqual(null);
expect(store.selectedEntity()).toEqual(null);

expect(store.todoEntityMap()).toEqual({ y: todo2, z: todo3 });
expect(store.todoIds()).toEqual(['y', 'z']);
expect(store.todoEntities()).toEqual([todo2, todo3]);
expect(store.todoSelectedId()).toEqual(null);
expect(store.todoSelectedEntity()).toEqual(null);
});
});
Loading

0 comments on commit aef2533

Please sign in to comment.