Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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