Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/signals/events/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src/index';
5 changes: 5 additions & 0 deletions modules/signals/events/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "index.ts"
}
}
36 changes: 36 additions & 0 deletions modules/signals/events/spec/dispatcher.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { TestBed } from '@angular/core/testing';
import { take } from 'rxjs';
import { type } from '@ngrx/signals';
import { Dispatcher, event, Events } from '../src';
import { ReducerEvents } from '../src/events-service';

describe('Dispatcher', () => {
it('is provided at the root level', () => {
const dispatcher = TestBed.inject(Dispatcher);
expect(dispatcher).toBeDefined();
});

it('emits dispatched events to the ReducerEvents service before the Events service', () => {
const dispatcher = TestBed.inject(Dispatcher);
const events = TestBed.inject(Events);
const reducerEvents = TestBed.inject(ReducerEvents);
const set = event('set', type<number>());
const result: Array<ReturnType<typeof set> & { order: number }> = [];

events
.on(set)
.pipe(take(1))
.subscribe((event) => result.push({ ...event, order: 2 }));
reducerEvents
.on(set)
.pipe(take(1))
.subscribe((event) => result.push({ ...event, order: 1 }));

dispatcher.dispatch(set(10));

expect(result).toEqual([
{ type: 'set', payload: 10, order: 1 },
{ type: 'set', payload: 10, order: 2 },
]);
});
});
55 changes: 55 additions & 0 deletions modules/signals/events/spec/event-creator-group.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { type } from '@ngrx/signals';
import { EventCreator, eventGroup } from '../src';

describe('eventGroup', () => {
it('creates a group of event creators', () => {
const counterPageEvents = eventGroup({
source: 'Counter Page',
events: {
increment: type<void>(),
decrement: type<void>(),
set: type<number>(),
},
});

const incrementEvent = counterPageEvents.increment();
const decrementEvent = counterPageEvents.decrement();
const setEvent = counterPageEvents.set(10);

expect(incrementEvent).toEqual({ type: '[Counter Page] increment' });
expect(decrementEvent).toEqual({ type: '[Counter Page] decrement' });
expect(setEvent).toEqual({ type: '[Counter Page] set', payload: 10 });
});

it('allows creating custom event group factories', () => {
function apiEventGroup<Source extends string, Entity>(
source: Source,
_entity: Entity
): {
loadedSuccess: EventCreator<`[${Source} API] loadedSuccess`, Entity[]>;
loadedFailure: EventCreator<`[${Source} API] loadedFailure`, void>;
} {
return eventGroup({
source: `${source} API`,
events: {
loadedSuccess: type<Entity[]>(),
loadedFailure: type<void>(),
},
});
}

type User = { id: number; name: string };
const usersApiEvents = apiEventGroup('Users', type<User>());

const loadedSuccessEvent = usersApiEvents.loadedSuccess([
{ id: 1, name: 'John Doe' },
]);
const loadedFailureEvent = usersApiEvents.loadedFailure();

expect(loadedSuccessEvent).toEqual({
type: '[Users API] loadedSuccess',
payload: [{ id: 1, name: 'John Doe' }],
});
expect(loadedFailureEvent).toEqual({ type: '[Users API] loadedFailure' });
});
});
26 changes: 26 additions & 0 deletions modules/signals/events/spec/event-creator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { type } from '@ngrx/signals';
import { event, EventCreator } from '../src';

describe('event', () => {
it('creates an event creator without payload', () => {
const increment = event('increment');
expect(increment()).toEqual({ type: 'increment' });
});

it('creates an event creator with payload', () => {
const set = event('set', type<{ count: number }>());
expect(set({ count: 10 })).toEqual({ type: 'set', payload: { count: 10 } });
});

it('allows creating custom event creator factories', () => {
function formattedEventCreator<Source extends string, Event extends string>(
source: Source,
eventName: Event
): EventCreator<`[${Source}] ${Event}`, void> {
return event(`[${source}] ${eventName}`);
}

const increment = formattedEventCreator('Counter Page', 'Increment');
expect(increment()).toEqual({ type: '[Counter Page] Increment' });
});
});
71 changes: 71 additions & 0 deletions modules/signals/events/spec/events-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { TestBed } from '@angular/core/testing';
import { type } from '@ngrx/signals';
import { Dispatcher, event, EventInstance, Events } from '../src';
import { SOURCE_TYPE } from '../src/events-service';

describe('Events', () => {
it('is provided at the root level', () => {
const events = TestBed.inject(Events);
expect(events).toBeDefined();
});

describe('on', () => {
const foo = event('foo');
const bar = event('bar', type<{ value: number }>());
const baz = event('baz');

it('emits events matching the provided event creators', () => {
const events = TestBed.inject(Events);
const dispatcher = TestBed.inject(Dispatcher);
const emittedEvents: EventInstance<string, unknown>[] = [];

events.on(foo, bar).subscribe((event) => emittedEvents.push(event));

dispatcher.dispatch(bar({ value: 10 }));
dispatcher.dispatch(foo());
dispatcher.dispatch(baz());
dispatcher.dispatch(bar({ value: 100 }));

expect(emittedEvents).toEqual([
{ type: 'bar', payload: { value: 10 } },
{ type: 'foo' },
{ type: 'bar', payload: { value: 100 } },
]);
});

it('emits all events when called without arguments', () => {
const events = TestBed.inject(Events);
const dispatcher = TestBed.inject(Dispatcher);
const emittedEvents: EventInstance<string, unknown>[] = [];

events.on().subscribe((event) => emittedEvents.push(event));

dispatcher.dispatch(foo());
dispatcher.dispatch(bar({ value: 10 }));
dispatcher.dispatch(baz());
dispatcher.dispatch(foo());

expect(emittedEvents).toEqual([
{ type: 'foo' },
{ type: 'bar', payload: { value: 10 } },
{ type: 'baz' },
{ type: 'foo' },
]);
});

it('adds SOURCE_TYPE to emitted events', () => {
const events = TestBed.inject(Events);
const dispatcher = TestBed.inject(Dispatcher);
const sourceTypes: string[] = [];

events
.on()
.subscribe((event) => sourceTypes.push((event as any)[SOURCE_TYPE]));

dispatcher.dispatch(foo());
dispatcher.dispatch(bar({ value: 10 }));

expect(sourceTypes).toEqual(['foo', 'bar']);
});
});
});
51 changes: 51 additions & 0 deletions modules/signals/events/spec/inject-dispatch.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { EnvironmentInjector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { type } from '@ngrx/signals';
import { Dispatcher, event, eventGroup, injectDispatch } from '../src';

describe('injectDispatch', () => {
it('creates self-dispatching events', () => {
const counterPageEvents = eventGroup({
source: 'Counter Page',
events: {
increment: type<void>(),
set: type<{ count: number }>(),
},
});
const dispatcher = TestBed.inject(Dispatcher);
const dispatch = TestBed.runInInjectionContext(() =>
injectDispatch(counterPageEvents)
);
vitest.spyOn(dispatcher, 'dispatch');

dispatch.increment();
expect(dispatcher.dispatch).toHaveBeenCalledWith({
type: '[Counter Page] increment',
});

dispatch.set({ count: 10 });
expect(dispatcher.dispatch).toHaveBeenCalledWith({
type: '[Counter Page] set',
payload: { count: 10 },
});
});

it('creates self-dispatching events with a custom injector', () => {
const increment = event('increment');
const injector = TestBed.inject(EnvironmentInjector);
const dispatcher = TestBed.inject(Dispatcher);
const dispatch = injectDispatch({ increment }, { injector });
vitest.spyOn(dispatcher, 'dispatch');

dispatch.increment();
expect(dispatcher.dispatch).toHaveBeenCalledWith({ type: 'increment' });
});

it('throws an error when called outside of an injection context', () => {
const increment = event('increment');

expect(() => injectDispatch({ increment })).toThrowError(
'injectDispatch() can only be used within an injection context'
);
});
});
Loading
Loading