Skip to content

Commit

Permalink
feat(core): add observed to OutputEmitterRef
Browse files Browse the repository at this point in the history
Prior to this commit, it wasn't possible to check whether the output had
any listeners (if it was being observed). Before `output()`, we could check
whether `EventEmitter` had any listeners by checking its `observed` property.

In this commit, we convert `listeners` from a list to a signal so that we can
have a computed `observed` signal.

This commit only updates the `OutputEmitterRef` implementation. We can't update
`OutputFromObservableRef` because it expects an observable signature to be
provided, and the observable doesn't technically have any API to count
subscribers; only `Subject` does.

Please note that there have been different discussions on whether the
`OutputEmitterRef` should be extended or not. This change is not considered
breaking because the signature of the _private_ property has been changed and
another property has been added to the public API.

Related issue: angular#54837
  • Loading branch information
arturovt committed May 14, 2024
1 parent 85ac2de commit 4205e3f
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 7 deletions.
28 changes: 21 additions & 7 deletions packages/core/src/authoring/output/output_emitter_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {ErrorHandler} from '../../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {DestroyRef} from '../../linker/destroy_ref';

import {signal} from '../../render3/reactivity/signal';
import {computed} from '../../render3/reactivity/computed';
import {OutputRef, OutputRefSubscription} from './output_ref';

/**
Expand All @@ -30,17 +32,23 @@ import {OutputRef, OutputRefSubscription} from './output_ref';
*/
export class OutputEmitterRef<T> implements OutputRef<T> {
private destroyed = false;
private listeners: Array<(value: T) => void> | null = null;
private listeners = signal<Array<(value: T) => void> | null>(null);
private errorHandler = inject(ErrorHandler, {optional: true});

/**
* The following signal indicates whether there are any active listeners
* registered for this output.
*/
readonly observed = computed(() => (this.listeners()?.length ?? 0) > 0);

/** @internal */
destroyRef: DestroyRef = inject(DestroyRef);

constructor() {
// Clean-up all listeners and mark as destroyed upon destroy.
this.destroyRef.onDestroy(() => {
this.destroyed = true;
this.listeners = null;
this.listeners.set(null);
});
}

Expand All @@ -54,13 +62,17 @@ export class OutputEmitterRef<T> implements OutputRef<T> {
);
}

(this.listeners ??= []).push(callback);
const listeners = this.listeners() ?? [];
listeners.push(callback);
this.listeners.set(listeners);

return {
unsubscribe: () => {
const idx = this.listeners?.indexOf(callback);
const listeners = this.listeners();
const idx = listeners?.indexOf(callback);
if (idx !== undefined && idx !== -1) {
this.listeners?.splice(idx, 1);
listeners!.splice(idx, 1);
this.listeners.set(listeners);
}
},
};
Expand All @@ -77,13 +89,15 @@ export class OutputEmitterRef<T> implements OutputRef<T> {
);
}

if (this.listeners === null) {
const listeners = this.listeners();

if (listeners === null) {
return;
}

const previousConsumer = setActiveConsumer(null);
try {
for (const listenerFn of this.listeners) {
for (const listenerFn of listeners) {
try {
listenerFn(value);
} catch (err: unknown) {
Expand Down
38 changes: 38 additions & 0 deletions packages/core/test/acceptance/authoring/output_function_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,4 +381,42 @@ describe('output() function', () => {
expect(triggered).toBe(1);
});
});

describe('observed()', () => {
it('should support counting listeners through observed()', () => {
@Component({
selector: 'app-dir',
template: 'Observed: {{ onBla.observed() }}',
standalone: true,
})
class Dir {
onBla = output<number>();
}

@Component({
template: `
@if (withListener) {
<app-dir (onBla)="true" />
} @else {
<app-dir />
}
`,
standalone: true,
imports: [Dir],
})
class App {
withListener = true;
}

const fixture = TestBed.createComponent(App);
fixture.detectChanges();

expect(fixture.nativeElement.innerHTML).toContain('Observed: true');

fixture.componentInstance.withListener = false;
fixture.detectChanges();

expect(fixture.nativeElement.innerHTML).toContain('Observed: false');
});
});
});

0 comments on commit 4205e3f

Please sign in to comment.