Skip to content

Commit e6d5632

Browse files
leonsenftpkozlowski-opensource
authored andcommitted
perf(core): tree shake unused dynamic [field] binding instructions (#65599)
Move the instructions used to dynamically bind a `Field` directive to a form control onto the `Field` itself. This way the instructions are only retained if the app uses the `Field` directive. PR Close #65599
1 parent cd7ae7e commit e6d5632

File tree

9 files changed

+81
-78
lines changed

9 files changed

+81
-78
lines changed

goldens/public-api/forms/signals/compat/index.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import { ValidatorFn } from '@angular/forms';
1818
import { WritableSignal } from '@angular/core';
1919
import { ɵCONTROL } from '@angular/core';
2020
import { ɵControl } from '@angular/core';
21+
import { ɵcontrolUpdate } from '@angular/core';
2122
import { ɵFieldState } from '@angular/core';
2223
import { ɵInteropControl } from '@angular/core';
24+
import { ɵɵcontrolCreate } from '@angular/core';
2325

2426
// @public
2527
export function compatForm<TModel>(model: WritableSignal<TModel>): FieldTree<TModel>;

goldens/public-api/forms/signals/index.api.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ import { ValidatorFn } from '@angular/forms';
2727
import { WritableSignal } from '@angular/core';
2828
import { ɵCONTROL } from '@angular/core';
2929
import { ɵControl } from '@angular/core';
30+
import { ɵcontrolUpdate } from '@angular/core';
3031
import { ɵFieldState } from '@angular/core';
3132
import { ɵInteropControl } from '@angular/core';
33+
import { ɵɵcontrolCreate } from '@angular/core';
3234

3335
// @public
3436
export function aggregateMetadata<TValue, TMetadataItem, TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>, key: AggregateMetadataKey<any, TMetadataItem>, logic: NoInfer<LogicFn<TValue, TMetadataItem, TPathKind>>): void;
@@ -144,7 +146,10 @@ export const FIELD: InjectionToken<Field<unknown>>;
144146
// @public
145147
export class Field<T> implements ɵControl<T> {
146148
// (undocumented)
147-
readonlyCONTROL]: undefined;
149+
readonlyCONTROL]: {
150+
readonly create: typeof ɵɵcontrolCreate;
151+
readonly update: typeof ɵcontrolUpdate;
152+
};
148153
// (undocumented)
149154
readonly classes: (readonly [string, i0.Signal<boolean>])[];
150155
// (undocumented)

packages/core/src/core_render3_private_export.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export {
9494
ɵɵcontentQuerySignal,
9595
ɵɵcontrol,
9696
ɵɵcontrolCreate,
97+
ɵcontrolUpdate,
9798
ɵɵcomponentInstance,
9899
ɵɵdefineComponent,
99100
ɵɵdefineDirective,

packages/core/src/render3/dynamic_bindings.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import {RuntimeError, RuntimeErrorCode} from '../errors';
1111
import {Type, Writable} from '../interface/type';
1212
import {assertNotDefined} from '../util/assert';
1313
import {bindingUpdated} from './bindings';
14-
import {ɵɵcontrolCreate as controlCreate, updateControl} from './instructions/control';
1514
import {setDirectiveInput, storePropertyBindingMetadata} from './instructions/shared';
15+
import {ɵCONTROL} from './interfaces/control';
16+
import {TNode} from './interfaces/node';
1617
import {TVIEW} from './interfaces/view';
1718
import {getCurrentTNode, getLView, getSelectedTNode, nextBindingIndex} from './state';
1819
import {stringifyForError} from './util/stringify_utils';
@@ -102,6 +103,23 @@ function inputBindingUpdate(targetDirectiveIdx: number, publicName: string, valu
102103
}
103104
}
104105

106+
/**
107+
* Instructions for dynamically binding a `Field` to a form control.
108+
*/
109+
interface ControlBinding {
110+
create: () => void;
111+
update: () => void;
112+
}
113+
114+
/**
115+
* Returns a {@link ControlBinding} for the target directive if it is a 'Field' directive.
116+
*/
117+
function controlBinding(binding: BindingInternal, tNode: TNode): ControlBinding | undefined {
118+
const lView = getLView();
119+
const directive = lView[tNode.directiveStart + binding.targetIdx!];
120+
return directive[ɵCONTROL];
121+
}
122+
105123
/**
106124
* Creates an input binding.
107125
* @param publicName Public name of the input to bind to.
@@ -125,15 +143,16 @@ export function inputBinding(publicName: string, value: () => unknown): Binding
125143
if (publicName === 'field') {
126144
const binding: BindingInternal = {
127145
[BINDING]: FIELD_BINDING_METADATA,
128-
create: () => controlCreate(),
146+
create: () => {
147+
// Set up the form control bindings, if this is a 'Field' directive bound to a form control.
148+
controlBinding(binding, getCurrentTNode()!)?.create();
149+
},
129150
update: () => {
130-
// Update the [field] input binding.
131-
inputBindingUpdate((binding as BindingInternal).targetIdx!, publicName, value());
151+
// Update the [field] input binding, regardless of whether this targets a 'Field' directive.
152+
inputBindingUpdate(binding.targetIdx!, publicName, value());
132153

133154
// Update the form control bindings, if this is a 'Field' directive bound to a form control.
134-
const lView = getLView();
135-
const tNode = getSelectedTNode();
136-
updateControl(lView, tNode);
155+
controlBinding(binding, getSelectedTNode()!)?.update();
137156
},
138157
};
139158
return binding;
@@ -143,7 +162,7 @@ export function inputBinding(publicName: string, value: () => unknown): Binding
143162
// don't get tree shaken when constructed by a function like this.
144163
const binding: BindingInternal = {
145164
[BINDING]: INPUT_BINDING_METADATA,
146-
update: () => inputBindingUpdate((binding as BindingInternal).targetIdx!, publicName, value()),
165+
update: () => inputBindingUpdate(binding.targetIdx!, publicName, value()),
147166
};
148167

149168
return binding;

packages/core/src/render3/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export {
9595
ɵɵproperty,
9696
ɵɵcontrol,
9797
ɵɵcontrolCreate,
98+
ɵcontrolUpdate,
9899
ɵɵcontentQuery,
99100
ɵɵcontentQuerySignal,
100101
ɵɵloadQuery,

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ export function ɵɵcontrolCreate(): void {
5656
}
5757

5858
const control = getControlDirective(tNode, lView);
59-
6059
if (!control) {
6160
return;
6261
}
@@ -102,12 +101,27 @@ export function ɵɵcontrol<T>(value: T, sanitizer?: SanitizerFn | null): void {
102101
}
103102

104103
/**
105-
* Updates a `ɵControl` to manage a native or custom form control.
104+
* Calls {@link updateControl} with the current `LView` and selected `TNode`.
105+
*
106+
* NOTE: This instruction exists solely to accommodate tree-shakeable, dynamic control bindings.
107+
* It's intended to be referenced exclusively by the Signal Forms `Field` directive and should not
108+
* be referenced by any other means.
109+
*/
110+
export function ɵcontrolUpdate(): void {
111+
const lView = getLView();
112+
const tNode = getSelectedTNode();
113+
updateControl(lView, tNode);
114+
}
115+
116+
/**
117+
* Updates the form control properties of a `field` bound form control.
118+
*
119+
* Does nothing if the current node is not a `field` bound form control.
106120
*
107121
* @param lView The `LView` that contains the control.
108122
* @param tNode The `TNode` of the control.
109123
*/
110-
export function updateControl<T>(lView: LView, tNode: TNode): void {
124+
function updateControl<T>(lView: LView, tNode: TNode): void {
111125
const control = getControlDirective(tNode, lView);
112126
if (control) {
113127
updateControlClasses(lView, tNode, control);

packages/core/src/render3/interfaces/control.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,25 @@ import {WritableSignal} from '../reactivity/signal';
1111
/** A unique symbol used to identify {@link ɵControl} implementations. */
1212
export const ɵCONTROL: unique symbol = Symbol('CONTROL');
1313

14+
/**
15+
* Instructions for dynamically binding a {@link ɵControl} to a form control.
16+
*/
17+
export interface ɵControlBinding {
18+
create(): void;
19+
update(): void;
20+
}
21+
1422
/**
1523
* A directive that binds a {@link ɵFieldState} to a form control.
1624
*/
1725
export interface ɵControl<T> {
18-
/** The presence of this property is used to identify {@link ɵControl} implementations. */
19-
readonly [ɵCONTROL]: undefined;
26+
/**
27+
* The presence of this property is used to identify {@link ɵControl} implementations, while the
28+
* value is used to store the instructions for dynamically binding to a form control. The
29+
* instructions are stored on the directive so that they can be tree-shaken when the directive is
30+
* not used.
31+
*/
32+
readonly [ɵCONTROL]: ɵControlBinding;
2033

2134
/** The state of the field bound to this control. */
2235
readonly state: Signal<ɵFieldState<T>>;

0 commit comments

Comments
 (0)