source$
Creates an event emitter with automatic cleanup and signal-based value tracking.
Overview
source$ provides a lightweight event streaming solution that combines:
- Event emission and subscription capabilities
- Automatic subscription cleanup via
DestroyRef - Signal-based value tracking for reactive access
- Optional last value preservation for late subscribers
- Read-only variants for encapsulation
Import
typescript
import { source$ } from '@craft-ng/core';Signature
typescript
function source$<T>(): Source$<T>;Returns
Source$<T> - An object with the following methods and properties:
emit(value: T)- Emits a value to all subscribers and updates the internal signalsubscribe(callback: (value: T) => void)- Subscribes to emissions with a callbackvalue: Signal<T | undefined>- A read-only signal containing the last emitted value (orundefinedif no value has been emitted)asReadonly()- Returns a read-only version of the source (onlysubscribeandvalue)preserveLastValue()- Returns a source variant that immediately emits the last value to new subscribers
Types
Source$
typescript
type Source$<T> = {
emit: (value: T) => void;
subscribe: (callback: (value: T) => void) => Subscription;
value: Signal<T | undefined>;
asReadonly: () => ReadonlySource$<T>;
preserveLastValue: () => {
emit: (value: T) => void;
subscribe: (callback: (value: T) => void) => void;
value: Signal<T | undefined>;
asReadonly: () => {
subscribe: (callback: (value: T) => void) => void;
value: Signal<T | undefined>;
};
};
};ReadonlySource$
typescript
type ReadonlySource$<T> = {
subscribe: (callback: (value: T) => void) => Subscription;
value: Signal<T | undefined>;
};Key Features
Automatic Cleanup
Subscriptions are automatically cleaned up when the injection context is destroyed, preventing memory leaks:
typescript
const userAction$ = source$<string>();
// Subscription is automatically unsubscribed on component destruction
userAction$.subscribe((action) => console.log(action));Signal Integration
The value property provides reactive access to the last emitted value:
typescript
const message$ = source$<string>();
message$.emit('Hello');
console.log(message$.value()); // 'Hello'
// Use in templates or computed signals
const uppercased = computed(() => message$.value()?.toUpperCase());Last Value Preservation
Use preserveLastValue() to ensure late subscribers receive the most recent value:
typescript
const counter$ = source$<number>();
counter$.emit(42);
// Standard source: late subscriber receives nothing
counter$.subscribe((v) => console.log('Standard:', v)); // Only future values
// With preserveLastValue: late subscriber gets the last value immediately
const preserved$ = counter$.preserveLastValue();
preserved$.subscribe((v) => console.log('Preserved:', v)); // Logs: Preserved: 42Common Patterns
Event Broadcasting
typescript
const buttonClick$ = source$<MouseEvent>();
// Multiple subscribers
buttonClick$.subscribe((event) => console.log('Logger:', event));
buttonClick$.subscribe((event) => trackEvent('button_click'));
// Emit events
button.addEventListener('click', (e) => buttonClick$.emit(e));Read-Only Access
typescript
class DataService {
private dataUpdated$ = source$<Data>();
// Expose read-only version
readonly dataUpdated = this.dataUpdated$.asReadonly();
updateData(data: Data) {
this.dataUpdated$.emit(data);
}
}Coordination with State
typescript
const resetTrigger$ = source$<void>();
const counter = state(0, ({ set, update }) => ({
increment: () => update((v) => v + 1),
decrement: () => update((v) => v - 1),
// Reset when source emits
reset: on$(resetTrigger$, () => set(0)),
}));Examples
Basic Usage with on$
typescript
import { source$, state, on$ } from '@craft-ng/core';
import { Component } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ counter() }}</p>
<button (click)="counter.increment()">+1</button>
<button (click)="counter.decrement()">-1</button>
<button (click)="reset$.emit()">Reset</button>
`,
standalone: true,
})
export class CounterComponent {
// Create a source for reset events
reset$ = source$<void>();
// Create state with automatic reset on source emission
counter = state(0, ({ set, update }) => ({
increment: () => update((v) => v + 1),
decrement: () => update((v) => v - 1),
// Internal method: listens to reset$ and sets counter to 0
// Not exposed on the state object (thanks to on$)
reset: on$(this.reset$, () => set(0)),
}));
}Multi-Source Coordination
typescript
import { source$, state, on$ } from '@craft-ng/core';
// Multiple sources for different events
const userLogin$ = source$<User>();
const userLogout$ = source$<void>();
const authState = state<User | null>(null, ({ set }) => ({
// Respond to multiple sources
onLogin: on$(userLogin$, (user) => set(user)),
onLogout: on$(userLogout$, () => set(null)),
}));
// Trigger events
userLogin$.emit({ id: 1, name: 'Alice' });
console.log(authState()); // { id: 1, name: 'Alice' }
userLogout$.emit();
console.log(authState()); // nullLate Subscriber Pattern
typescript
import { source$ } from '@craft-ng/core';
const notifications$ = source$<string>().preserveLastValue();
// Emit before any subscribers
notifications$.emit('Server started');
notifications$.emit('Database connected');
// Late subscriber receives the last value immediately
setTimeout(() => {
notifications$.subscribe((msg) => {
console.log('Late subscriber:', msg); // Logs: Late subscriber: Database connected
});
}, 1000);Related
- on$ - Subscribe to sources with automatic cleanup in state insertions