Skip to content

Commit c687b8f

Browse files
devversionpkozlowski-opensource
authored andcommitted
feat(core): expose new output() API (angular#54650)
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 angular#54650
1 parent 9ce6277 commit c687b8f

File tree

9 files changed

+52
-38
lines changed

9 files changed

+52
-38
lines changed

goldens/public-api/core/errors.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ export const enum RuntimeErrorCode {
105105
// (undocumented)
106106
NO_SUPPORTING_DIFFER_FACTORY = 901,
107107
// (undocumented)
108+
OUTPUT_REF_DESTROYED = 953,
109+
// (undocumented)
108110
PIPE_NOT_FOUND = -302,
109111
// (undocumented)
110112
PLATFORM_ALREADY_DESTROYED = 404,

goldens/public-api/core/index.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,13 +1266,41 @@ export interface Output {
12661266
// @public (undocumented)
12671267
export const Output: OutputDecorator;
12681268

1269+
// @public
1270+
export function output<T = void>(opts?: OutputOptions): OutputEmitterRef<T>;
1271+
12691272
// @public
12701273
export interface OutputDecorator {
12711274
(alias?: string): any;
12721275
// (undocumented)
12731276
new (alias?: string): any;
12741277
}
12751278

1279+
// @public
1280+
export class OutputEmitterRef<T> implements OutputRef<T> {
1281+
constructor();
1282+
emit(value: T): void;
1283+
// (undocumented)
1284+
subscribe(callback: (value: T) => void): OutputRefSubscription;
1285+
}
1286+
1287+
// @public
1288+
export interface OutputOptions {
1289+
// (undocumented)
1290+
alias?: string;
1291+
}
1292+
1293+
// @public
1294+
export interface OutputRef<T> {
1295+
subscribe(callback: (value: T) => void): OutputRefSubscription;
1296+
}
1297+
1298+
// @public
1299+
export interface OutputRefSubscription {
1300+
// (undocumented)
1301+
unsubscribe(): void;
1302+
}
1303+
12761304
// @public @deprecated
12771305
export const PACKAGE_ROOT_URL: InjectionToken<string>;
12781306

packages/compiler-cli/src/ngtsc/typecheck/test/output_function_diagnostics.spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ runInEachFileSystem(() => {
1616
const testCases: TestCase[] = [
1717
{
1818
id: 'basic output',
19-
outputs: {'evt': {type: 'OutputEmitter<string>'}},
19+
outputs: {'evt': {type: 'OutputEmitterRef<string>'}},
2020
template: `<div dir (evt)="$event.bla">`,
2121
expected: [
2222
`TestComponent.html(1, 24): Property 'bla' does not exist on type 'string'.`,
2323
],
2424
},
2525
{
2626
id: 'output with void type',
27-
outputs: {'evt': {type: 'OutputEmitter<void>'}},
27+
outputs: {'evt': {type: 'OutputEmitterRef<void>'}},
2828
template: `<div dir (evt)="$event.x">`,
2929
expected: [
3030
`TestComponent.html(1, 24): Property 'x' does not exist on type 'void'.`,
@@ -33,7 +33,7 @@ runInEachFileSystem(() => {
3333
{
3434
id: 'two way data binding, invalid',
3535
inputs: {'value': {type: 'InputSignal<string>', isSignal: true}},
36-
outputs: {'valueChange': {type: 'OutputEmitter<string>'}},
36+
outputs: {'valueChange': {type: 'OutputEmitterRef<string>'}},
3737
template: `<div dir [(value)]="bla">`,
3838
component: `bla = true;`,
3939
expected: [
@@ -43,14 +43,14 @@ runInEachFileSystem(() => {
4343
{
4444
id: 'two way data binding, valid',
4545
inputs: {'value': {type: 'InputSignal<string>', isSignal: true}},
46-
outputs: {'valueChange': {type: 'OutputEmitter<string>'}},
46+
outputs: {'valueChange': {type: 'OutputEmitterRef<string>'}},
4747
template: `<div dir [(value)]="bla">`,
4848
component: `bla: string = ''`,
4949
expected: [],
5050
},
5151
{
5252
id: 'complex output object',
53-
outputs: {'evt': {type: 'OutputEmitter<{works: boolean}>'}},
53+
outputs: {'evt': {type: 'OutputEmitterRef<{works: boolean}>'}},
5454
template: `<div dir (evt)="x = $event.works">`,
5555
component: `x: never = null!`, // to raise a diagnostic to check the type.
5656
expected: [
@@ -62,7 +62,7 @@ runInEachFileSystem(() => {
6262
id: 'mixing decorator-based and initializer-based outputs',
6363
outputs: {
6464
evt1: {type: 'EventEmitter<string>'},
65-
evt2: {type: 'OutputEmitter<string>'},
65+
evt2: {type: 'OutputEmitterRef<string>'},
6666
},
6767
template: `<div dir (evt1)="x1 = $event" (evt2)="x2 = $event">`,
6868
component: `
@@ -77,13 +77,13 @@ runInEachFileSystem(() => {
7777
// restricted fields
7878
{
7979
id: 'allows access to private output',
80-
outputs: {evt: {type: 'OutputEmitter<string>', restrictionModifier: 'private'}},
80+
outputs: {evt: {type: 'OutputEmitterRef<string>', restrictionModifier: 'private'}},
8181
template: `<div dir (evt)="true">`,
8282
expected: [],
8383
},
8484
{
8585
id: 'allows access to protected output',
86-
outputs: {evt: {type: 'OutputEmitter<string>', restrictionModifier: 'protected'}},
86+
outputs: {evt: {type: 'OutputEmitterRef<string>', restrictionModifier: 'protected'}},
8787
template: `<div dir (evt)="true">`,
8888
expected: [],
8989
},

packages/compiler-cli/src/ngtsc/typecheck/test/test_case_helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export function typeCheckDiagnose(c: TestCase, compilerOptions?: ts.CompilerOpti
7171
import {
7272
InputSignal,
7373
EventEmitter,
74-
OutputEmitter,
74+
OutputEmitterRef,
7575
InputSignalWithTransform,
7676
ModelSignal,
7777
WritableSignal,

packages/core/src/authoring.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,7 @@ export {InputOptions, InputOptionsWithoutTransform, InputOptionsWithTransform, I
1414
export {ɵUnwrapDirectiveSignalInputs} from './authoring/input/input_type_checking';
1515
export {ModelFunction} from './authoring/model/model';
1616
export {ModelOptions, ModelSignal} from './authoring/model/model_signal';
17-
export {output as ɵoutput, OutputEmitter as ɵOutputEmitter, OutputOptions as ɵOutputOptions} from './authoring/output';
17+
export {output, OutputOptions} from './authoring/output/output';
18+
export {getOutputDestroyRef as ɵgetOutputDestroyRef, OutputEmitterRef} from './authoring/output/output_emitter_ref';
19+
export {OutputRef, OutputRefSubscription} from './authoring/output/output_ref';
1820
export {ContentChildFunction, ViewChildFunction} from './authoring/queries';

packages/core/src/authoring/output.ts renamed to packages/core/src/authoring/output/output.ts

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {EventEmitter} from '../event_emitter';
10-
11-
/**
12-
* An `OutputEmitter` is created by the `output()` function and can be
13-
* used to emit values to consumers of your directive or component.
14-
*
15-
* Consumers of your directive/component can bind to the output and
16-
* subscribe to changes via the bound event syntax. For example:
17-
*
18-
* ```html
19-
* <my-comp (valueChange)="processNewValue($event)" />
20-
* ```
21-
*
22-
* @developerPreview
23-
*/
24-
export interface OutputEmitter<T> {
25-
emit(value: T): void;
26-
27-
// TODO: Consider exposing `subscribe` for dynamically created components.
28-
/** @internal */
29-
subscribe(listener: (v: T) => void): void;
30-
}
9+
import {OutputEmitterRef} from './output_emitter_ref';
3110

3211
/**
3312
* Options for declaring an output.
@@ -52,13 +31,13 @@ export interface OutputOptions {
5231
* ```ts
5332
* @Directive({..})
5433
* export class MyDir {
55-
* nameChange = output<string>(); // OutputEmitter<string>
56-
* onClick = output(); // OutputEmitter<void>
34+
* nameChange = output<string>(); // OutputEmitterRef<string>
35+
* onClick = output(); // OutputEmitterRef<void>
5736
* }
5837
* ```
5938
*
6039
* @developerPreview
6140
*/
62-
export function output<T = void>(opts?: OutputOptions): OutputEmitter<T> {
63-
return new EventEmitter();
41+
export function output<T = void>(opts?: OutputOptions): OutputEmitterRef<T> {
42+
return new OutputEmitterRef<T>();
6443
}

packages/core/test/bundling/defer/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1670,6 +1670,9 @@
16701670
{
16711671
"name": "init_output"
16721672
},
1673+
{
1674+
"name": "init_output_emitter_ref"
1675+
},
16731676
{
16741677
"name": "init_partial"
16751678
},

packages/core/test/bundling/router/bundle.golden_symbols.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1989,7 +1989,7 @@
19891989
"name": "tap"
19901990
},
19911991
{
1992-
"name": "throwError"
1992+
"name": "throwError2"
19931993
},
19941994
{
19951995
"name": "throwIfEmpty"

packages/language-service/test/type_definitions_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ describe('type definitions', () => {
106106
project, {templateOverride: `<my-dir (name¦Changes)="doSmth()" />`});
107107
expect(definitions!.length).toEqual(1);
108108

109-
assertTextSpans(definitions, ['OutputEmitter']);
109+
assertTextSpans(definitions, ['OutputEmitterRef']);
110110
assertFileNames(definitions, ['index.d.ts']);
111111
});
112112
});

0 commit comments

Comments
 (0)