Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finalizing output() and outputFromObservable() APIs #54650

Closed
wants to merge 18 commits into from

Conversation

devversion
Copy link
Member

@devversion devversion commented Feb 28, 2024

See individual commits.

———

output()

This commit exposes the new output() API with numerous benefits:

  • Symmetrical API to input(), model() etc.
  • Fixed types for EventEmitter.emit— current emit method of
    EventEmitter is broken and accepts undefined via emit(value?: T)
  • Removal of RxJS specific concepts from outputs. error channels,
    completion channels etc. We now have a simple consistent
    interface.
  • Automatic clean-up of subscribers upon directive/component destory-
    when subscribed programmatically.
import {output} from '@angular/core';

@Directive({..})
export class MyDir {
  nameChange = output<string>();     // OutputEmitterRef<string>
  onClick = output();                // OutputEmitterRef<void>

  doSomething() {
    this.onClick.emit();
  }
}

Note: RxJS custom observable cases will be handled in future commits via
explicit helpers from the interop.

outputFromObservable()

import {outputFromObservable} from '@angular/core/rxjs-interop';

 @Directive({..})
 export class MyDir {
   nameChange$ = new BehavorSubject<string>();
   nameChange = outputFromObservable(this.nameChange$); // OutputRef<string>
}

outputToObservable

import {outputToObservable} from '@angular/core/rxjs-interop';


outputToObservable(myInstance.someOutput)
  .pipe(take(1))
  .subscribe(...)

@angular-robot angular-robot bot added detected: feature PR contains a feature commit area: build & ci Related the build and CI infrastructure of the project labels Feb 28, 2024
@ngbot ngbot bot added this to the Backlog milestone Feb 28, 2024
@devversion devversion force-pushed the outputs-api branch 2 times, most recently from c0f485e to 894a4e0 Compare February 28, 2024 16:22
@devversion devversion added the action: review The PR is still awaiting reviews from at least one requested reviewer label Feb 28, 2024
@angular-robot angular-robot bot removed the area: build & ci Related the build and CI infrastructure of the project label Feb 28, 2024
@ngbot ngbot bot removed this from the Backlog milestone Feb 28, 2024
@devversion devversion marked this pull request as ready for review February 28, 2024 17:28
@devversion devversion added the target: minor This PR is targeted for the next minor release label Feb 28, 2024
@pkozlowski-opensource pkozlowski-opensource added the area: core Issues related to the framework runtime label Feb 28, 2024
@ngbot ngbot bot added this to the Backlog milestone Feb 28, 2024
@markostanimirovic
Copy link
Contributor

markostanimirovic commented Feb 28, 2024

A great addition to the rxjs-interop plugin! 🚀 However, function names seem too verbose to me. Consider the following naming:

  • outputFromObservable => rxOutput
  • outputToObservable => add an additional signature to the existing toObservable function that accepts OutputRef as input argument
-import { outputFromObservable } from '@angular/core/rxjs-interop';
+import { rxOutput } from '@angular/core/rxjs-interop';

@Directive({..})
export class MyDir {
  nameChange$ = new Subject<string>();
- nameChange = outputFromObservable(this.nameChange$);
+ nameChange = rxOutput(this.nameChange$);
}

EDIT: Another suggestion - outputFromObservable(obs$) => toOutput(obs$)

tmpFs.mount(tmpFs.resolve('/node_modules/@angular'), angularFolder.get());
}

// TODO: Consider removing.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Burn it all 🔥

packages/core/src/authoring/output/output_ref.ts Outdated Show resolved Hide resolved
packages/core/src/authoring/output/output_ref.ts Outdated Show resolved Hide resolved
packages/core/rxjs-interop/src/output_from_observable.ts Outdated Show resolved Hide resolved
packages/core/rxjs-interop/src/output_from_observable.ts Outdated Show resolved Hide resolved
packages/core/rxjs-interop/src/output_to_observable.ts Outdated Show resolved Hide resolved
packages/core/src/event_emitter.ts Outdated Show resolved Hide resolved
// Attempt to retrieve a `DestroyRef` optionally.
// For backwards compatibility reasons, this cannot be required.
try {
this.__internal_DestroyRef = inject(DestroyRef);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not inject(DestroyRef, {optional: true})?

Or are you worried about EventEmitters created outside of DI contexts? I would recommend avoiding the try/catch and checking getInjectImplementation() || getCurrentInjector() (see impl of assertInInjectionContext.

Also, have we checked in g3 whether making EventEmitter depend on injection context is that breaking?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because EventEmitter may be constructed outside of an injection context, yes.

Started a TGP to check how breaking it would be.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Results: 40k failures, unfeasible. Most issues arise from QueryList lazily creating an EventEmitter (?!) lazily for .changes. This can happen outside of injection contexts and affects a lot of targets

packages/core/src/event_emitter.ts Outdated Show resolved Hide resolved

// Stop yielding more values when the directive/component is already destroyed.
const subscription =
observable.pipe(takeUntil(onDestroy)).subscribe(value => callbackFn(value));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to do something with the error/complete channels here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I think we should just document that users are responsible for handling errors beforehand. If they aren't, the error will be simply handled by RxJS and will be reported unhandled to window.onerror (see RxJS reportUnhandledError.ts)

packages/core/src/authoring/model/model.ts Outdated Show resolved Hide resolved
packages/core/src/authoring/output/output_ref.ts Outdated Show resolved Hide resolved
packages/core/src/authoring/output/output_ref.ts Outdated Show resolved Hide resolved
packages/core/src/authoring/output/output_ref.ts Outdated Show resolved Hide resolved
@twittwer
Copy link

I really like the suggestion from @markostanimirovic to use the same naming pattern as the signal interop with toOutput & toObervable.

And if the extension of toObervable is a problem maybe fromOutput.

Copy link
Member

@pkozlowski-opensource pkozlowski-opensource left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. As discussed in person I'm not sure about the completion semantics of the output as Observable and - more importantly - EventEmitter involvement in it. I would lean towards dropping the completion logic but at the same time I don't feel like I understand all the consequences here.

Reviewed-for: public-api
Reviewed-for: fw-core

@brandonroberts
Copy link
Contributor

I agree with toOutput to be consistent with the other helper functions. This is the other direction, but I recall Ben Lesh providing some feedback against using the fromThing convention for naming.

Also from an import perspective, you might hit output much more frequently when trying to get to outputFromObservable when using autocomplete because of the naming similarity. toOutput being similar to the others is more intentional when trying to use it.

@devversion devversion force-pushed the outputs-api branch 3 times, most recently from a75d9c5 to 32fd5e4 Compare March 1, 2024 08:27
@devversion devversion added action: merge The PR is ready for merge by the caretaker merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Mar 6, 2024
@pkozlowski-opensource
Copy link
Member

This PR was merged into the repository by commit d4154f9.

pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
This commit replaces `fake_core` with the real `@angular/core`
output. See previous commit for reasons.

Overall, this commit:

* Replaces references of `fake_core`
* Fixes tests that were testing Angular compiler detection that _would_
  already be flagged by type-checking of TS directly. We keep these
  tests for now, and add `@ts-ignore` to verify the Angular checks, in
  case type checking is disabled in user applications- but it's worth
  considering to remove these tests. Follow-up question/non-priority.
* Adds `@ts-ignore` to the tests for `defer` 1P because the property is
  marked as `@internal` and now is (correctly) causing failures in the
  compiler test environment.
* Fixes a couple of tests with typos, wrong properties etc that
  previously weren't detected! A good sign.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
This commit creates the proposed `OutputRef` interface along
with `OutputEmitterRef`:

- `OutputRef` is the consistent interface for all Angular outputs.
- `OutputEmitterRef` is an extension for emitting values. Like
  `EventEmitter`.
- subscriptions are cleaned up automatically upon directive/component
  destroy.
- emitting is an error when destroyed
- subscribing programmatically is an error when already destroyed.

This commit will also implement the shared output ref runtime construct,
that can be used by `output()`, `outputFromObservable()` and `model()`.

We will manage subscriptions in a simple way, manually, without RxJS.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
This commit exposes the new `output()` API with numerous benefits:

- Symmetrical API to `input()`, `model()` etc.
- Fixed types for `EventEmitter.emit`— current `emit` method of
  `EventEmitter` is broken and accepts `undefined` via `emit(value?: T)`
- Removal of RxJS specific concepts from outputs. error channels,
  completion channels etc. We now have a simple consistent
  interface.
- Automatic clean-up of subscribers upon directive/component destory-
  when subscribed programmatically.

```ts
@directive({..})
export class MyDir {
  nameChange = output<string>();     // OutputEmitterRef<string>
  onClick = output();                // OutputEmitterRef<void>
}
```

Note: RxJS custom observable cases will be handled in future commits via
explicit helpers from the interop.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
…rent modules (#54650)

This commit allows us to detect initializer APIs like
`outputFromObservable` that are declared in different modules- not
necessarily `@angular/core`.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
Introduces a second API in addition to the new `output()` function.

The new function `outputFromObservable()` can be used to declare outputs
using the new `OutputRef` API and `output()` API, while using a custom
RxJS observable as data source.

This is something that is currently possible in Angular and we would
like to keep possible- even though we never intended to support custom
observables aside from RxJS-based `EventEmitter`.

The interop bridges the gap and allows you to continue using
`Subject`, `ReplaySubject`, `BehaivorSubjct,` - or cold custom
observables for outputs. You can still trigger logic only when
the output is subscribed- unlike with imperative `emit`s of
`EventEmitter` or the new `OutputEmitterRef`.

A notable difference is that you need two class members where you
previously could access the `Subject` directly. This is an intentional
trade-off we've made to ensure that all new outputs implement the
`OutputRef` interface and we are exposing a minimal API surface to
consumers of components that currently access the output
programmatically.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
This commit introduces an addition to `output()` and
`outputFromObservable`()` —called `outputToObservable()`.

The helper lives in the RxJS interop package and allows agnostic
programmatic subscriptions to `OutputRef`s by converting the output
to an observable with `.pipe` etc.

The function is ideally used in all places where you subscribe to an
output programmatically. Those outputs in the future, with the new APIs,
may not be actual RxJS constructs, but abstract `OutputRef`'s that
simply expose a `.subscribe` method. The helper allows you to
agnostically convert outputs to RxJS observables that you can safely
interact with.

The observables are also completed automatically, if possible, when the
owning directive/component is destroyed— Something that is not
guaranteed right now.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
This commit adds compliance tests for the new output APIs.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
A model signal is technically an output, at runtime and conceptually.

This commit re-uses the shared output ref logic and ensures the
interfaces match.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
An `EventEmitter` is a construct owned by Angular that should be
used for outputs as of right now.

As we are introducing the new `OutputRef` interface for the new output
function APIs, we also think `EventEmitter` should implement
`OutputRef`— ensuring all "known" outputs follow the same contract.

This commit ensures `EventEmitter` implements an `OutputRef`

Note: An output ref captures the destroy ref from the current injection
context for clean-up purposes. This is also done for `EventEmitter` in a
backwards compatible way:

- not requiring an injection context. EventEmitter may be used
  elsewhere.
- not cleaning up subscriptions/completing the emitter when the
  directive/component is destroyed. This would be a change in behavior.

Note 2: The dependency on `DestroyRef` causes it to be retained in all
bundling examples because ironically `NgZone` uses `EventEmitter`- not
for outputs. The code is pretty minimal though, so that should be
acceptable.

`EventEmitter` will now always retain `NgZone. This increases the
payload size slightly around 800b for AIO. Note that the other increases
were coming from previous changes. This commit just pushed it over the
threshold.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
Adds runtime acceptance tests for `output()` and
`outputFromObservable()`.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
…ion context (#54650)

Technically `model()` and `output()` already need to be defined in an
injection context- because `OutputRef` requires this.

To improve the error messaging, this commit asserts this as part of the
top-level entry functions for `model()` and `output()`. Without this
change, the error would mention the `_createOutputRef` internal
function.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
Adds additional language-service tests for `outputFromObservable()`.
Existing tests already verify the behavior for `output()`.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
The `inject` global augmentation from upgrade tests, leak into
all source files for IDEs, making it easy to run into issues
when actually trying to deal with `inject` from Angular core for DI.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
…4650)

Adds additional ngtsc compiler tests for the `outputFromObservable` API.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
Adds an integration test for `output()` and `outputFromObservable()`.
The test verifies the JIT transform as well.

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
…54650)

Currently the `makeProgram` utility from `ngtsc/testing` does not use
the test host by default- optimizing for source file caching.

Additionally, the host can be updated to attempt caching of the `.d.ts`
files from `@angular/core`— whether that's fake core, or the real core-
is irrelevant. We are never caching if these changes between tests, so
correctness is guaranteed.

This commit reduces the type check test times form 80s to just 11
seconds, faster than what it was before with `fake_core`. The ngtsc
tests also run significantly faster. From 40s to 30s

PR Close #54650
pkozlowski-opensource pushed a commit that referenced this pull request Mar 6, 2024
For model signals we introduced some sniffing on the return type of a
`.subscribe` invocation- allowing for subscribe to _just_ return a
callback directly to unsubscribe.

This works in practice, but the positive `tCleanup` indices have more
meaning, especially in the context of `DebugElement`. A positive index
indicates a DOM event- so we need to revert this change. This now
surfaced as we made `EventEmitter` return a function + the subscription
via a proxy that ended up `typeof function` --> and broke some tests
where debug element incorrectly invoked non-dom outputs as dom
listeners. We don't need this change with current unsubscribe function
concept.

PR Close #54650
@TomieAi
Copy link

TomieAi commented Mar 11, 2024

.required when? I love some vscode yelling I forgot something.

@Flo0806
Copy link

Flo0806 commented Mar 24, 2024

.required when? I love some vscode yelling I forgot something.

The new output is the replacement for EventEmitter(). And it would be not a good idea to have a required option. Think: You don't need the click event on every button. But if you must use it, it would just annoy the hell out of you.

In the end: a input is different - here it makes sense to use required if needed, but not on a output/EventEmitter.

Greetings, Flo

@blogcraft
Copy link

Why output() does not use signals like the new input()?

@JoostK
Copy link
Member

JoostK commented Apr 1, 2024

Why output() does not use signals like the new input()?

It represents an event, i.e. an action at some point in time, rather than state, i.e. stored data. Signals correspond with state storage, not events.

@angular-automatic-lock-bot
Copy link

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.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators May 2, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: core Issues related to the framework runtime detected: feature PR contains a feature commit merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note PullApprove: disable target: minor This PR is targeted for the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet