Content-Length: 787731 | pFad | https://github.com/angular/angular/commit/be44cc8f40fb2364dbaf20ba24496e4355f84e78

D6 feat(core): support listening to outputs on dynamically-created compo… · angular/angular@be44cc8 · GitHub
Skip to content

Commit be44cc8

Browse files
crisbetommalerba
authored andcommitted
feat(core): support listening to outputs on dynamically-created components (#60137)
Adds the new `outputBinding` function that allows users to listen to outputs on dynamically-created components in a similar way to templates. For example, here we create an instance of `MyCheckbox` and listen to its `onChange` event: ```ts interface CheckboxChange { value: string; } createComponent(MyCheckbox, { bindings: [ outputBinding<CheckboxChange>('onChange', event => console.log(event.value)) ], }); ``` Note that while it has always been possible to listen to events like this by getting a hold of of the instance and subscribing to it, there are a few key differences: 1. `outputBinding` behaves in the same way as if the event was bound in a template which comes with some behaviors like forwarding errors to the `ErrorHandler` and marking the view as dirty. 2. With `outputBinding` the listeners will be cleaned up automatically when the component is destroyed. 3. `outputBinding` accounts for host directive outputs by binding to them through the host. E.g. if the `onChange` event above was coming from a host directive, `outputBinding` would bind to it automatically. Currently `outputBinding` is available only in `createComponent`, `ViewContainerRef.createComponent` and `ComponentFactory.create`, but it will serve as a base for APIs in the future. PR Close #60137
1 parent 3459faa commit be44cc8

File tree

17 files changed

+425
-5
lines changed

17 files changed

+425
-5
lines changed

goldens/public-api/core/index.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,6 +1363,9 @@ export const Output: OutputDecorator;
13631363
// @public
13641364
export function output<T = void>(opts?: OutputOptions): OutputEmitterRef<T>;
13651365

1366+
// @public
1367+
export function outputBinding<T>(eventName: string, listener: (event: T) => unknown): Binding;
1368+
13661369
// @public
13671370
export interface OutputDecorator {
13681371
(alias?: string): any;

goldens/size-tracking/integration-payloads.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"cli-hello-world": {
33
"uncompressed": {
4-
"main": 132425,
4+
"main": 137893,
55
"polyfills": 33792
66
}
77
},

packages/core/src/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export {
112112
afterNextRender,
113113
ɵFirstAvailable,
114114
} from './render3/after_render/hooks';
115-
export {inputBinding} from './render3/dynamic_bindings';
115+
export {inputBinding, outputBinding} from './render3/dynamic_bindings';
116116
export {ApplicationConfig, mergeApplicationConfig} from './application/application_config';
117117
export {makeStateKey, StateKey, TransferState} from './transfer_state';
118118
export {booleanAttribute, numberAttribute} from './util/coercion';

packages/core/src/render3/dynamic_bindings.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
import {RuntimeError, RuntimeErrorCode} from '../errors';
1010
import {Type} from '../interface/type';
1111
import {bindingUpdated} from './bindings';
12+
import {listenToDirectiveOutput, wrapListener} from './instructions/listener';
1213
import {setDirectiveInput, storePropertyBindingMetadata} from './instructions/shared';
1314
import {DirectiveDef} from './interfaces/definition';
14-
import {getLView, getSelectedTNode, getTView, nextBindingIndex} from './state';
15+
import {CONTEXT} from './interfaces/view';
16+
import {getCurrentTNode, getLView, getSelectedTNode, getTView, nextBindingIndex} from './state';
1517

1618
/** Symbol used to store and retrieve metadata about a binding. */
1719
export const BINDING = /* @__PURE__ */ Symbol('BINDING');
@@ -47,8 +49,9 @@ export interface DirectiveWithBindings<T> {
4749
bindings: Binding[];
4850
}
4951

50-
// This is constant between all the bindings so we can reuse the object.
52+
// These are constant between all the bindings so we can reuse the objects.
5153
const INPUT_BINDING_METADATA: Binding[typeof BINDING] = {kind: 'input', requiredVars: 1};
54+
const OUTPUT_BINDING_METADATA: Binding[typeof BINDING] = {kind: 'output', requiredVars: 0};
5255

5356
class InputBinding<T> implements Binding {
5457
readonly target!: DirectiveDef<T>;
@@ -89,6 +92,46 @@ class InputBinding<T> implements Binding {
8992
}
9093
}
9194

95+
class OutputBinding<T> implements Binding {
96+
readonly target!: DirectiveDef<unknown>;
97+
readonly [BINDING] = OUTPUT_BINDING_METADATA;
98+
99+
constructor(
100+
private readonly eventName: string,
101+
private readonly listener: (event: T) => unknown,
102+
) {}
103+
104+
create(): void {
105+
if (!this.target && ngDevMode) {
106+
throw new RuntimeError(
107+
RuntimeErrorCode.NO_BINDING_TARGET,
108+
`Output binding to "${this.eventName}" does not have a target.`,
109+
);
110+
}
111+
112+
const lView = getLView<{} | null>();
113+
const tView = getTView();
114+
const tNode = getCurrentTNode()!;
115+
const context = lView[CONTEXT];
116+
const wrappedListener = wrapListener(tNode, lView, context, this.listener);
117+
const hasBound = listenToDirectiveOutput(
118+
tNode,
119+
tView,
120+
lView,
121+
this.target,
122+
this.eventName,
123+
wrappedListener,
124+
);
125+
126+
if (!hasBound && ngDevMode) {
127+
throw new RuntimeError(
128+
RuntimeErrorCode.INVALID_BINDING_TARGET,
129+
`${this.target.type.name} does not have an output with a public name of "${this.eventName}".`,
130+
);
131+
}
132+
}
133+
}
134+
92135
/**
93136
* Creates an input binding.
94137
* @param publicName Public name of the input to bind to.
@@ -110,3 +153,28 @@ class InputBinding<T> implements Binding {
110153
export function inputBinding<T>(publicName: string, value: () => unknown): Binding {
111154
return new InputBinding<T>(publicName, value);
112155
}
156+
157+
/**
158+
* Creates an output binding.
159+
* @param eventName Public name of the output to listen to.
160+
* @param listener Function to be called when the output emits.
161+
*
162+
* ### Usage example
163+
* In this example we create an instance of the `MyCheckbox` component and listen
164+
* to its `onChange` event.
165+
*
166+
* ```
167+
* interface CheckboxChange {
168+
* value: string;
169+
* }
170+
*
171+
* createComponent(MyCheckbox, {
172+
* bindings: [
173+
* outputBinding<CheckboxChange>('onChange', event => console.log(event.value))
174+
* ],
175+
* });
176+
* ```
177+
*/
178+
export function outputBinding<T>(eventName: string, listener: (event: T) => unknown): Binding {
179+
return new OutputBinding<T>(eventName, listener);
180+
}

packages/core/src/render3/instructions/listener.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ function executeListenerWithErrorHandling(
328328
* @param wrapWithPreventDefault Whether or not to prevent default behavior
329329
* (the procedural renderer does this already, so in those cases, we should skip)
330330
*/
331-
function wrapListener(
331+
export function wrapListener(
332332
tNode: TNode,
333333
lView: LView<{} | null>,
334334
context: {} | null,
@@ -378,3 +378,80 @@ function isOutputSubscribable(value: unknown): value is SubscribableOutput<unkno
378378
value != null && typeof (value as Partial<SubscribableOutput<unknown>>).subscribe === 'function'
379379
);
380380
}
381+
382+
/** Listens to an output on a specific directive. */
383+
export function listenToDirectiveOutput(
384+
tNode: TNode,
385+
tView: TView,
386+
lView: LView,
387+
target: DirectiveDef<unknown>,
388+
eventName: string,
389+
listenerFn: (e?: any) => any,
390+
): boolean {
391+
const tCleanup = tView.firstCreatePass ? getOrCreateTViewCleanup(tView) : null;
392+
const lCleanup = getOrCreateLViewCleanup(lView);
393+
let hostIndex: number | null = null;
394+
let hostDirectivesStart: number | null = null;
395+
let hostDirectivesEnd: number | null = null;
396+
let hasOutput = false;
397+
398+
if (ngDevMode && !tNode.directiveToIndex?.has(target.type)) {
399+
throw new Error(`Node does not have a directive with type ${target.type.name}`);
400+
}
401+
402+
const data = tNode.directiveToIndex!.get(target.type)!;
403+
404+
if (typeof data === 'number') {
405+
hostIndex = data;
406+
} else {
407+
[hostIndex, hostDirectivesStart, hostDirectivesEnd] = data;
408+
}
409+
410+
if (
411+
hostDirectivesStart !== null &&
412+
hostDirectivesEnd !== null &&
413+
tNode.hostDirectiveOutputs?.hasOwnProperty(eventName)
414+
) {
415+
const hostDirectiveOutputs = tNode.hostDirectiveOutputs[eventName];
416+
417+
for (let i = 0; i < hostDirectiveOutputs.length; i += 2) {
418+
const index = hostDirectiveOutputs[i] as number;
419+
420+
if (index >= hostDirectivesStart && index <= hostDirectivesEnd) {
421+
ngDevMode && assertIndexInRange(lView, index);
422+
hasOutput = true;
423+
listenToOutput(
424+
tNode,
425+
tView,
426+
lView,
427+
index,
428+
hostDirectiveOutputs[i + 1] as string,
429+
eventName,
430+
listenerFn,
431+
lCleanup,
432+
tCleanup,
433+
);
434+
} else if (index > hostDirectivesEnd) {
435+
break;
436+
}
437+
}
438+
}
439+
440+
if (hostIndex !== null && target.outputs.hasOwnProperty(eventName)) {
441+
ngDevMode && assertIndexInRange(lView, hostIndex);
442+
hasOutput = true;
443+
listenToOutput(
444+
tNode,
445+
tView,
446+
lView,
447+
hostIndex,
448+
eventName,
449+
eventName,
450+
listenerFn,
451+
lCleanup,
452+
tCleanup,
453+
);
454+
}
455+
456+
return hasOutput;
457+
}

0 commit comments

Comments
 (0)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: https://github.com/angular/angular/commit/be44cc8f40fb2364dbaf20ba24496e4355f84e78

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy