angular
angular copied to clipboard
Add observed property to OutputEmitterRef
Which @angular/* package(s) are relevant/related to the feature request?
core
Description
With the @Output decorator, I used to check the observed of the EventEmitter (because it inherits Subject) to enable/disable some features.
For example, <lu-select (clueChange)="..." /> would display a search bar in LuSelect component but <lu-select /> would not.
This solution seems elegant as it avoid an additional input that would lead to unwanted cases:
- Both
<lu-select enableClue (clueChange)="..." />and<lu-select />would work <lu-select (clueChange)="..." />would be useless because the search bar is not disabled becauseenableClue = false<lu-select enableClue />would be useless because the search bar is displayed, but nobody listen to its value
Proposed solution
Add a getterproperty observed in OutputEmitterRef
export class OutputEmitterRef<T> implements OutputRef<T> {
private listeners: Array<(value: T) => void>|null = null;
get observed(): boolean {
return !!this.listeners?.length;
}
//...
}
Alternatives considered
Replace existing usages of emitter.observed by inputs.
If we did expose such a property, it would probably be a Signal<boolean>.
It would be even better!
What about output.required? which would error if no listeners would be registred? - of course I can create separate issue if needed, but it seems like aother solution to the same problem.
The output would need to be required only when an input has a specific value (if I understand what is on your mind)
Yeah, Now I see difrence, in your proposition itβs more about conditional required based on value of some input / directive. So yeah I will open new issue as both solutions solve difrent issues. Sory for clogging this issue
Yeah, this is one of the follow-up's we did consider and would implement using a Signal<boolean> as Alex pointed out.
For the use-cases, we did see a similar pattern, especially with expensive subscriptions only being triggered upon "interest from the parent".
The initial thinking still was that an input likely makes more sense here. Subjectively, it would seem confusing to me if the behavior of a component changes based on an output that I subscribe to. That is because "inputs" can "configure" the directive, but outputs simply emit values to the parent conceptually. It's more explicit and keeps the mental model simpler IMO.
In either case, adding such a signal to OutputRef is trivial.
One use case for this is wrapping third party (non angular) libraries. For example if such library allow binding to mouse move event and I want to allow my component user to add a (for example) chart mouse move output I will need to always listen to the third party event. If I could know if output was declared I could optimize and won't listen to that event unnecessarily. An input here doesn't make sense as the one using my component shouldn't configure internal optimization based on how the wrapped third party works.
Very interesting issue. Thanks @GuillaumeNury for raising this.
TL;DR: I wouldn't recommend changing OutputRef API for this as there are decent solutions but if we really go that road than we have to think about what we really want to expose (a boolean? the count? the listeners?)
I totally agree with @devversion on this except on this point:
Subjectively, it would seem confusing to me if the behavior of a component changes based on an output that I subscribe to.
where I think that this is not subjective, but really objective π
First, I think that it makes outputs align with DOM event listeners which do not affect DOM behavior. Additionally, this assumption is also an enabler for testing tools or dev tools. For example, some dev/testing tool could track all component outputs to debug them manually or to compare them to a previous run etc...
In the case of performance optimization, I think that outputFromObservable is a the best alternative to subscribe lazily to expensive sources (like @Harpush's example of a 3rd party mouse tracking lib).
That is why I'd generally recommend being explicit and use inputs in Guillaume's case.
This way we can reduce the OutputRef's API surface and keep it simple.
Meanwhile, if you really want to go with magic π, or if it is really needed to debug or optimize something, the solution could be a SnitchyEventEmitter π: https://stackblitz.com/edit/angular-output-snitching?file=src%2Fmain.ts
If some additional API should be added to OutputRef, then we have to think of whether it's a boolean value or the count of listeners or maybe the listeners?
Indeed, a component might want to throw an error if an output is subscribed to more than once (this can happen through imperative APIs).
For the listeners themselves, I don't have a use case, but for some testing scenarios (that I wouldn't recommend), one might want to trigger a listener from the test.
As observed is not likely to change (from the template you cannot subscribe from an output and then cancel the cancellation), I can use a simple Subject and outputFromObservable to achieve what I need:
@Component({
standalone: true,
selector: 'app-child',
template: `
<h2>{{ actionEmitter.observed ? 'π' : 'π' }}</h2>
<button
[disabled]="!actionEmitter.observed"
(click)="actionEmitter.next()">CLICK</button>
`,
})
export class Child {
actionEmitter = new Subject<void>();
action = outputFromObservable(this.actionEmitter);
}
(observed is not a signal anymore but a regular boolean. Subject already track its observers)
Stackblitz : https://stackblitz.com/edit/angular-output-snitching-wd3hf4?file=src%2Fmain.ts
Thanks @yjaaidi for the pointer (and the stackblits)!
As the workaround looks simple, maybe we can close this issue?
Cool! Happy to hear that this works for you.
is not likely to change (from the template you cannot subscribe from an output and then cancel the cancellation Indeed.
Not likely but still possible in case there is an imperative subscription (e.g. testing or viewChild() usage or dynamic creation). So in that case, the view might not be updated if using ChangeDetectionStrategy.OnPush (or this would cause in error in the future signal-based components).
Just an observation here for anyone reading this in the future. Not all observables have the observed property.
In case the source is not a Subject but an Observable (e.g. FormGroup#valueChanges), then you might need something similar to countObservers operator from the stackblitz above.
i also would love to have the observed prop back. Although not extremely common, we do do this in several places. In this places, it will prevent us from being able to switch to output() syntax. outputFromObservable would work, but it's just not very convenient and not something I want to propagate everywhere we do it.
I need the observed property back, my use case that I forward a stream to an output, but before I subscribe to the stream I want check if the output if observed, would prefer to avoid subscription if user did not use the output.
I'd like to add my usecase where I find convenient to have the observed boolean.
I develop a DesignSystem and a big custom component provides outputs (xyzClicked) when the user clicks on a nested component.
If the output is observed, i add a ripple and hover styles. Whereas, if it is not observed, the nested component is kind of readonly.
There is already like 10 or more inputs, I'd rather not adding more inputs just to say "this is clickable" because the html already implies it should be clickable through the (xyzClicked).
+1
Just adding that without the observed property I need either the same amount of observables or the same amount of inputs. That's annoying and will probably make some people stick with decorator for now. It is such a small addition to the API surface so I don't understand why not... Especially when there are valid use cases
+1
+1
I was migrating a project from using @Input() and @Output() to signal-based input and output when I came across a component where I used @if(onSearch.observed) to render a block of HTML for the search feature.
It seems there's no similar approach when using signal-based output.
+1
Never spam +1 on GitHub, use the Subscribe button and/or emojis. You're annoying maintainers and everyone here with notifications.
We discussed it with the team. For now to address this usecase, we recommend using outputFromObservable as an observable exposes sub observed property.
Example:
@Component({
selector: 'app-child',
template: `This is a child cmp`,
})
export class Child {
mySub1 = new Subject<number>();
myOutput = outputFromObservable(this.mySub1);
constructor() {
let count = 0;
setInterval(() => {
if (this.mySub1.observed) {
console.log('emit');
this.mySub1.next(count++);
} else {
console.log('not observed');
}
}, 1000);
}
}
@Component({
selector: 'app-root',
imports: [Child],
template: `<app-child/>`,
})
export class App {
something(event: any) {
console.log('something', event);
}
}
We might revisit this if/when the native observables are standardised.
This issue has been automatically locked due to inactivity. Please file a new issue if you are encountering a similar or related problem.
Read more about our automatic conversation locking policy.
This action has been performed automatically by a bot.