-
Notifications
You must be signed in to change notification settings - Fork 26.2k
/
Copy pathdynamic_bindings.ts
203 lines (183 loc) Β· 7.03 KB
/
dynamic_bindings.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {WritableSignal} from '../core_reactivity_export_internal';
import {RuntimeError, RuntimeErrorCode} from '../errors';
import {Type, Writable} from '../interface/type';
import {assertNotDefined} from '../util/assert';
import {bindingUpdated} from './bindings';
import {setDirectiveInput, storePropertyBindingMetadata} from './instructions/shared';
import {TVIEW} from './interfaces/view';
import {getCurrentTNode, getLView, getSelectedTNode, nextBindingIndex} from './state';
import {stringifyForError} from './util/stringify_utils';
import {createOutputListener} from './view/directive_outputs';
/** Symbol used to store and retrieve metadata about a binding. */
export const BINDING = /* @__PURE__ */ Symbol('BINDING');
/**
* A dynamically-defined binding targeting.
* For example, `inputBinding('value', () => 123)` creates an input binding.
*/
export interface Binding {
readonly [BINDING]: {
readonly kind: string;
readonly requiredVars: number;
};
/** Target index (in a view's registry) to which to apply the binding. */
readonly targetIdx?: number;
/** Callback that will be invoked during creation. */
create?(): void;
/** Callback that will be invoked during updates. */
update?(): void;
}
/**
* Represents a dynamically-created directive with bindings targeting it specifically.
*/
export interface DirectiveWithBindings<T> {
/** Directive type that should be created. */
type: Type<T>;
/** Bindings that should be applied to the specific directive. */
bindings: Binding[];
}
// These are constant between all the bindings so we can reuse the objects.
const INPUT_BINDING_METADATA: Binding[typeof BINDING] = {kind: 'input', requiredVars: 1};
const OUTPUT_BINDING_METADATA: Binding[typeof BINDING] = {kind: 'output', requiredVars: 0};
// TODO(pk): this is a sketch of an input binding instruction that still needs some cleanups
// - take an index of a directive on TNode (as matched), review all the index mappings that we need to do
// - move more logic to the first creation pass
// - move this function to under the instructions folder
function inputBindingUpdate(targetDirectiveIdx: number, publicName: string, value: unknown) {
const lView = getLView();
const bindingIndex = nextBindingIndex();
if (bindingUpdated(lView, bindingIndex, value)) {
const tView = lView[TVIEW];
const tNode = getSelectedTNode();
// TODO(pk): don't check on each and every binding, just assert in dev mode
const targetDef = tView.directiveRegistry![targetDirectiveIdx];
if (ngDevMode && !targetDef) {
throw new RuntimeError(
RuntimeErrorCode.NO_BINDING_TARGET,
`Input binding to property "${publicName}" does not have a target.`,
);
}
// TODO(pk): the hasSet check should be replaced by one-off check in the first creation pass
const hasSet = setDirectiveInput(tNode, tView, lView, targetDef, publicName, value);
if (ngDevMode) {
if (!hasSet) {
throw new RuntimeError(
RuntimeErrorCode.NO_BINDING_TARGET,
`${stringifyForError(targetDef.type)} does not have an input with a public name of "${publicName}".`,
);
}
storePropertyBindingMetadata(tView.data, tNode, publicName, bindingIndex);
}
}
}
/**
* Creates an input binding.
* @param publicName Public name of the input to bind to.
* @param value Callback that returns the current value for the binding. Can be either a signal or
* a plain getter function.
*
* ### Usage Example
* In this example we create an instance of the `MyButton` component and bind the value of
* the `isDisabled` signal to its `disabled` input.
*
* ```
* const isDisabled = signal(false);
*
* createComponent(MyButton, {
* bindings: [inputBinding('disabled', isDisabled)]
* });
* ```
*/
export function inputBinding(publicName: string, value: () => unknown): Binding {
// Note: ideally we would use a class here, but it seems like they
// don't get tree shaken when constructed by a function like this.
const binding: Binding = {
[BINDING]: INPUT_BINDING_METADATA,
update: () => inputBindingUpdate(binding.targetIdx!, publicName, value()),
};
return binding;
}
/**
* Creates an output binding.
* @param eventName Public name of the output to listen to.
* @param listener Function to be called when the output emits.
*
* ### Usage example
* In this example we create an instance of the `MyCheckbox` component and listen
* to its `onChange` event.
*
* ```
* interface CheckboxChange {
* value: string;
* }
*
* createComponent(MyCheckbox, {
* bindings: [
* outputBinding<CheckboxChange>('onChange', event => console.log(event.value))
* ],
* });
* ```
*/
export function outputBinding<T>(eventName: string, listener: (event: T) => unknown): Binding {
// Note: ideally we would use a class here, but it seems like they
// don't get tree shaken when constructed by a function like this.
const binding: Binding = {
[BINDING]: OUTPUT_BINDING_METADATA,
create: () => {
const lView = getLView<{} | null>();
const tNode = getCurrentTNode()!;
const tView = lView[TVIEW];
const targetDef = tView.directiveRegistry![binding.targetIdx!];
createOutputListener(tNode, lView, listener, targetDef, eventName);
},
};
return binding;
}
/**
* Creates a two-way binding.
* @param eventName Public name of the two-way compatible input.
* @param value Writable signal from which to get the current value and to which to write new
* values.
*
* ### Usage example
* In this example we create an instance of the `MyCheckbox` component and bind to its `value`
* input using a two-way binding.
*
* ```
* const checkboxValue = signal('');
*
* createComponent(MyCheckbox, {
* bindings: [
* twoWayBinding('value', checkboxValue),
* ],
* });
* ```
*/
export function twoWayBinding(publicName: string, value: WritableSignal<unknown>): Binding {
const input = inputBinding(publicName, value);
const output = outputBinding(publicName + 'Change', (eventValue) => value.set(eventValue));
// We take advantage of inputs only having a `create` block and outputs only having an `update`
// block by passing them through directly instead of creating dedicated functions here. This
// assumption can break down if one of them starts targeting both blocks. These assertions
// are here to help us catch it if something changes in the future.
ngDevMode && assertNotDefined(input.create, 'Unexpected `create` callback in inputBinding');
ngDevMode && assertNotDefined(output.update, 'Unexpected `update` callback in outputBinding');
return {
[BINDING]: {
kind: 'twoWay',
requiredVars: input[BINDING].requiredVars + output[BINDING].requiredVars,
},
set targetIdx(idx: number) {
(input as Writable<Binding>).targetIdx = idx;
(output as Writable<Binding>).targetIdx = idx;
},
create: output.create,
update: input.update,
};
}