@thomas-siegfried/tapout 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +1205 -1205
- package/package.json +55 -55
- package/src/applyBindings.ts +372 -372
- package/src/arrayToDomMapping.ts +300 -300
- package/src/attributeInterpolationMarkup.ts +91 -91
- package/src/bindingContext.ts +207 -207
- package/src/bindingEvent.ts +198 -198
- package/src/bindingProvider.ts +260 -260
- package/src/bindings.ts +963 -963
- package/src/compareArrays.ts +122 -122
- package/src/componentBinding.ts +318 -318
- package/src/componentDecorator.ts +62 -62
- package/src/components.ts +367 -367
- package/src/computed.ts +439 -439
- package/src/configure.ts +52 -52
- package/src/controlFlowBindings.ts +206 -206
- package/src/core.ts +60 -60
- package/src/decorators.ts +407 -407
- package/src/dependencyDetection.ts +76 -76
- package/src/disposable.ts +36 -36
- package/src/domData.ts +42 -42
- package/src/domNodeDisposal.ts +94 -94
- package/src/effects.ts +29 -29
- package/src/event.ts +173 -173
- package/src/expressionRewriting.ts +219 -219
- package/src/extenders.ts +102 -102
- package/src/filters.ts +91 -91
- package/src/index.ts +150 -150
- package/src/interpolationMarkup.ts +130 -130
- package/src/memoization.ts +71 -71
- package/src/namespacedBindings.ts +132 -132
- package/src/observable.ts +48 -48
- package/src/observableArray.ts +397 -397
- package/src/options.ts +25 -25
- package/src/selectExtensions.ts +68 -68
- package/src/slotBinding.ts +84 -84
- package/src/subscribable.ts +266 -266
- package/src/tasks.ts +79 -79
- package/src/templateEngine.ts +108 -108
- package/src/templateRendering.ts +399 -399
- package/src/templateRewriting.ts +94 -94
- package/src/templateSources.ts +134 -134
- package/src/utils.ts +123 -123
- package/src/utilsDom.ts +87 -87
- package/src/virtualElements.ts +153 -153
- package/src/wireParams.ts +49 -49
package/src/componentBinding.ts
CHANGED
|
@@ -1,318 +1,318 @@
|
|
|
1
|
-
import { Computed } from './computed.js';
|
|
2
|
-
import { ignore } from './dependencyDetection.js';
|
|
3
|
-
import { addDisposeCallback } from './domNodeDisposal.js';
|
|
4
|
-
import { bindingEvent } from './bindingEvent.js';
|
|
5
|
-
import { BindingProvider, bindingHandlers, setCustomElementHooks, addNodePreprocessor } from './bindingProvider.js';
|
|
6
|
-
import type { BindingHandler } from './bindingProvider.js';
|
|
7
|
-
import { BindingContext } from './bindingContext.js';
|
|
8
|
-
import { applyBindingsToDescendants } from './applyBindings.js';
|
|
9
|
-
import {
|
|
10
|
-
virtualChildNodes,
|
|
11
|
-
virtualEmptyNode,
|
|
12
|
-
virtualSetChildren,
|
|
13
|
-
allowedVirtualElementBindings,
|
|
14
|
-
} from './virtualElements.js';
|
|
15
|
-
import { unwrapObservable } from './utils.js';
|
|
16
|
-
import { cloneNodes } from './utilsDom.js';
|
|
17
|
-
import { isReadableSubscribable } from './subscribable.js';
|
|
18
|
-
import { Observable } from './observable.js';
|
|
19
|
-
import { parseObjectLiteral } from './expressionRewriting.js';
|
|
20
|
-
import { getObservable } from './decorators.js';
|
|
21
|
-
import { wireParams } from './wireParams.js';
|
|
22
|
-
import { options } from './options.js';
|
|
23
|
-
import * as components from './components.js';
|
|
24
|
-
import type { ComponentDefinition, ComponentInfo } from './components.js';
|
|
25
|
-
|
|
26
|
-
let componentLoadingOperationUniqueId = 0;
|
|
27
|
-
|
|
28
|
-
function cloneTemplateIntoElement(
|
|
29
|
-
componentName: string,
|
|
30
|
-
componentDefinition: ComponentDefinition,
|
|
31
|
-
element: Node,
|
|
32
|
-
): void {
|
|
33
|
-
const template = componentDefinition.template;
|
|
34
|
-
if (!template) {
|
|
35
|
-
throw new Error("Component '" + componentName + "' has no template");
|
|
36
|
-
}
|
|
37
|
-
const clonedNodesArray = cloneNodes(template);
|
|
38
|
-
virtualSetChildren(element, clonedNodesArray);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function createViewModel(
|
|
42
|
-
componentDefinition: ComponentDefinition,
|
|
43
|
-
componentParams: unknown,
|
|
44
|
-
componentInfo: ComponentInfo,
|
|
45
|
-
): unknown {
|
|
46
|
-
const factory = componentDefinition.createViewModel;
|
|
47
|
-
return factory
|
|
48
|
-
? factory.call(componentDefinition, componentParams, componentInfo)
|
|
49
|
-
: componentParams;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const componentHandler: BindingHandler = {
|
|
53
|
-
init(element, valueAccessor, _allBindings, _viewModel, bindingContext) {
|
|
54
|
-
let currentViewModel: unknown;
|
|
55
|
-
let currentLoadingOperationId: number | null;
|
|
56
|
-
let afterRenderSub: { dispose(): void } | null = null;
|
|
57
|
-
|
|
58
|
-
function disposeAssociatedComponentViewModel() {
|
|
59
|
-
const vm = currentViewModel as { dispose?: () => void } | null;
|
|
60
|
-
if (typeof vm?.dispose === 'function') {
|
|
61
|
-
vm.dispose();
|
|
62
|
-
}
|
|
63
|
-
if (afterRenderSub) {
|
|
64
|
-
afterRenderSub.dispose();
|
|
65
|
-
}
|
|
66
|
-
afterRenderSub = null;
|
|
67
|
-
currentViewModel = null;
|
|
68
|
-
currentLoadingOperationId = null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const originalChildNodes = Array.from(virtualChildNodes(element));
|
|
72
|
-
|
|
73
|
-
virtualEmptyNode(element);
|
|
74
|
-
addDisposeCallback(element, disposeAssociatedComponentViewModel);
|
|
75
|
-
|
|
76
|
-
const computed = new Computed(() => {
|
|
77
|
-
const value = unwrapObservable(valueAccessor());
|
|
78
|
-
let componentName: string;
|
|
79
|
-
let componentParams: unknown;
|
|
80
|
-
|
|
81
|
-
if (typeof value === 'string') {
|
|
82
|
-
componentName = value;
|
|
83
|
-
} else {
|
|
84
|
-
const obj = value as { name: unknown; params?: unknown };
|
|
85
|
-
componentName = unwrapObservable(obj.name) as string;
|
|
86
|
-
componentParams = unwrapObservable(obj.params);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!componentName) {
|
|
90
|
-
throw new Error('No component name specified');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const asyncContext = bindingEvent.startPossiblyAsyncContentBinding(element, bindingContext);
|
|
94
|
-
|
|
95
|
-
const loadingOperationId = currentLoadingOperationId = ++componentLoadingOperationUniqueId;
|
|
96
|
-
|
|
97
|
-
components.get(componentName, (componentDefinition) => {
|
|
98
|
-
if (currentLoadingOperationId !== loadingOperationId) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
disposeAssociatedComponentViewModel();
|
|
103
|
-
|
|
104
|
-
if (!componentDefinition) {
|
|
105
|
-
throw new Error("Unknown component '" + componentName + "'");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
cloneTemplateIntoElement(componentName, componentDefinition, element);
|
|
109
|
-
|
|
110
|
-
const componentInfo: ComponentInfo = {
|
|
111
|
-
element,
|
|
112
|
-
templateNodes: originalChildNodes,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const componentViewModel = createViewModel(componentDefinition, componentParams, componentInfo);
|
|
116
|
-
|
|
117
|
-
if (componentViewModel && componentViewModel !== componentParams &&
|
|
118
|
-
typeof componentParams === 'object' && componentParams !== null) {
|
|
119
|
-
wireParams(componentViewModel as object, componentParams as Record<string, unknown>, element);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (componentViewModel && typeof (componentViewModel as Record<string, unknown>).onInit === 'function') {
|
|
123
|
-
((componentViewModel as Record<string, unknown>).onInit as () => void)();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const childBindingContext = asyncContext.createChildContext(componentViewModel, {
|
|
127
|
-
extend(ctx: BindingContext) {
|
|
128
|
-
ctx['$component'] = componentViewModel;
|
|
129
|
-
ctx['$componentTemplateNodes'] = originalChildNodes;
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
if (componentViewModel && typeof (componentViewModel as Record<string, unknown>).onDescendantsComplete === 'function') {
|
|
134
|
-
afterRenderSub = bindingEvent.subscribe(
|
|
135
|
-
element,
|
|
136
|
-
bindingEvent.descendantsComplete,
|
|
137
|
-
(componentViewModel as Record<string, unknown>).onDescendantsComplete as (value: unknown) => void,
|
|
138
|
-
componentViewModel,
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
currentViewModel = componentViewModel;
|
|
143
|
-
ignore(() => applyBindingsToDescendants(childBindingContext, element));
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
addDisposeCallback(element, () => computed.dispose());
|
|
148
|
-
|
|
149
|
-
return { controlsDescendantBindings: true };
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
bindingHandlers['component'] = componentHandler;
|
|
154
|
-
allowedVirtualElementBindings['component'] = true;
|
|
155
|
-
|
|
156
|
-
// ---- Custom Element Bridge ----
|
|
157
|
-
|
|
158
|
-
export function getComponentNameForNode(node: Node): string | undefined {
|
|
159
|
-
if (node.nodeType !== 1) return undefined;
|
|
160
|
-
const tagNameLower = ((node as Element).tagName || '').toLowerCase();
|
|
161
|
-
if (!components.isRegistered(tagNameLower)) return undefined;
|
|
162
|
-
if (tagNameLower.indexOf('-') !== -1 || ('' + node) === '[object HTMLUnknownElement]') {
|
|
163
|
-
return tagNameLower;
|
|
164
|
-
}
|
|
165
|
-
return undefined;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function isWritable(value: unknown): boolean {
|
|
169
|
-
if (value instanceof Observable) return true;
|
|
170
|
-
if (value instanceof Computed) return (value as Computed<unknown>).hasWriteFunction;
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const nativeProviderInstance = new BindingProvider();
|
|
175
|
-
|
|
176
|
-
const CONTEXT_KEYWORDS = new Set([
|
|
177
|
-
'$data', '$rawData', '$root', '$parent', '$parentContext',
|
|
178
|
-
'$parents', '$component', '$componentTemplateNodes', '$index',
|
|
179
|
-
]);
|
|
180
|
-
|
|
181
|
-
const OBSERVABLE_REF_PATTERN = /^\$([a-zA-Z_$][\w$]*)$/;
|
|
182
|
-
|
|
183
|
-
function getComponentParamsFromCustomElement(
|
|
184
|
-
elem: Element,
|
|
185
|
-
bindingContext: BindingContext,
|
|
186
|
-
): Record<string, unknown> {
|
|
187
|
-
const paramsAttribute = elem.getAttribute('params');
|
|
188
|
-
|
|
189
|
-
if (paramsAttribute) {
|
|
190
|
-
const keyValuePairs = parseObjectLiteral(paramsAttribute);
|
|
191
|
-
const observableRefParams: Record<string, string> = {};
|
|
192
|
-
const normalPairs: string[] = [];
|
|
193
|
-
|
|
194
|
-
for (const kv of keyValuePairs) {
|
|
195
|
-
const key = kv.key || kv.unknown || '';
|
|
196
|
-
const val = (kv.value || '').trim();
|
|
197
|
-
const match = val.match(OBSERVABLE_REF_PATTERN);
|
|
198
|
-
|
|
199
|
-
if (match && !CONTEXT_KEYWORDS.has(val)) {
|
|
200
|
-
observableRefParams[key] = match[1];
|
|
201
|
-
} else if (key) {
|
|
202
|
-
normalPairs.push(key + ':' + (kv.value || ''));
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const result: Record<string, unknown> = {};
|
|
207
|
-
const rawParamComputedValues: Record<string, Computed<unknown>> = {};
|
|
208
|
-
|
|
209
|
-
if (normalPairs.length > 0) {
|
|
210
|
-
const normalParamsString = normalPairs.join(',');
|
|
211
|
-
const params = nativeProviderInstance.parseBindingsString(
|
|
212
|
-
normalParamsString,
|
|
213
|
-
bindingContext,
|
|
214
|
-
elem,
|
|
215
|
-
{ valueAccessors: true },
|
|
216
|
-
) as Record<string, () => unknown> | null;
|
|
217
|
-
|
|
218
|
-
if (params) {
|
|
219
|
-
for (const paramName of Object.keys(params)) {
|
|
220
|
-
rawParamComputedValues[paramName] = new Computed(
|
|
221
|
-
params[paramName] as () => unknown,
|
|
222
|
-
);
|
|
223
|
-
addDisposeCallback(elem, () => rawParamComputedValues[paramName].dispose());
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
for (const paramName of Object.keys(rawParamComputedValues)) {
|
|
227
|
-
const paramValueComputed = rawParamComputedValues[paramName];
|
|
228
|
-
const paramValue = paramValueComputed.peek();
|
|
229
|
-
|
|
230
|
-
if (!paramValueComputed.isActive()) {
|
|
231
|
-
result[paramName] = paramValue;
|
|
232
|
-
} else {
|
|
233
|
-
result[paramName] = new Computed({
|
|
234
|
-
read() {
|
|
235
|
-
return unwrapObservable(paramValueComputed.get());
|
|
236
|
-
},
|
|
237
|
-
write: isWritable(paramValue)
|
|
238
|
-
? (value: unknown) => {
|
|
239
|
-
const currentObs = paramValueComputed.get();
|
|
240
|
-
if (currentObs instanceof Observable) {
|
|
241
|
-
currentObs.set(value);
|
|
242
|
-
} else if (currentObs instanceof Computed && (currentObs as Computed<unknown>).hasWriteFunction) {
|
|
243
|
-
(currentObs as Computed<unknown>).set(value);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
: undefined,
|
|
247
|
-
});
|
|
248
|
-
addDisposeCallback(elem, () => (result[paramName] as Computed<unknown>).dispose());
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
for (const [key, propName] of Object.entries(observableRefParams)) {
|
|
255
|
-
const dataContext = bindingContext.$data;
|
|
256
|
-
if (dataContext && typeof dataContext === 'object') {
|
|
257
|
-
const obs = getObservable(dataContext as object, propName);
|
|
258
|
-
if (obs) {
|
|
259
|
-
result[key] = obs;
|
|
260
|
-
} else {
|
|
261
|
-
result[key] = (dataContext as Record<string, unknown>)[propName];
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (!Object.prototype.hasOwnProperty.call(result, '$raw')) {
|
|
267
|
-
result['$raw'] = rawParamComputedValues;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return result;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return { $raw: {} };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export function addBindingsForCustomElement(
|
|
277
|
-
allBindings: Record<string, unknown> | null,
|
|
278
|
-
node: Node,
|
|
279
|
-
bindingContext: BindingContext,
|
|
280
|
-
valueAccessors?: boolean,
|
|
281
|
-
): Record<string, unknown> | null {
|
|
282
|
-
if (node.nodeType === 1) {
|
|
283
|
-
const componentName = getComponentNameForNode(node);
|
|
284
|
-
if (componentName) {
|
|
285
|
-
allBindings = allBindings || {};
|
|
286
|
-
|
|
287
|
-
if (allBindings['component']) {
|
|
288
|
-
throw new Error('Cannot use the "component" binding on a custom element matching a component');
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const componentBindingValue = {
|
|
292
|
-
name: componentName,
|
|
293
|
-
params: getComponentParamsFromCustomElement(node as Element, bindingContext),
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
allBindings['component'] = valueAccessors
|
|
297
|
-
? () => componentBindingValue
|
|
298
|
-
: componentBindingValue;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return allBindings;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Register hooks on the binding provider to enable custom element detection
|
|
306
|
-
setCustomElementHooks(getComponentNameForNode, addBindingsForCustomElement);
|
|
307
|
-
|
|
308
|
-
// Apply display:contents to custom elements so they don't affect layout.
|
|
309
|
-
// Opt out per-element with the "layout" attribute, or globally via options.customElementDisplayContents.
|
|
310
|
-
addNodePreprocessor((node: Node): Node[] | void => {
|
|
311
|
-
if (!options.customElementDisplayContents) return;
|
|
312
|
-
if (node.nodeType === 1 && node.nodeName.includes('-')) {
|
|
313
|
-
const el = node as HTMLElement;
|
|
314
|
-
if (!el.hasAttribute('layout')) {
|
|
315
|
-
el.style.display = 'contents';
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
});
|
|
1
|
+
import { Computed } from './computed.js';
|
|
2
|
+
import { ignore } from './dependencyDetection.js';
|
|
3
|
+
import { addDisposeCallback } from './domNodeDisposal.js';
|
|
4
|
+
import { bindingEvent } from './bindingEvent.js';
|
|
5
|
+
import { BindingProvider, bindingHandlers, setCustomElementHooks, addNodePreprocessor } from './bindingProvider.js';
|
|
6
|
+
import type { BindingHandler } from './bindingProvider.js';
|
|
7
|
+
import { BindingContext } from './bindingContext.js';
|
|
8
|
+
import { applyBindingsToDescendants } from './applyBindings.js';
|
|
9
|
+
import {
|
|
10
|
+
virtualChildNodes,
|
|
11
|
+
virtualEmptyNode,
|
|
12
|
+
virtualSetChildren,
|
|
13
|
+
allowedVirtualElementBindings,
|
|
14
|
+
} from './virtualElements.js';
|
|
15
|
+
import { unwrapObservable } from './utils.js';
|
|
16
|
+
import { cloneNodes } from './utilsDom.js';
|
|
17
|
+
import { isReadableSubscribable } from './subscribable.js';
|
|
18
|
+
import { Observable } from './observable.js';
|
|
19
|
+
import { parseObjectLiteral } from './expressionRewriting.js';
|
|
20
|
+
import { getObservable } from './decorators.js';
|
|
21
|
+
import { wireParams } from './wireParams.js';
|
|
22
|
+
import { options } from './options.js';
|
|
23
|
+
import * as components from './components.js';
|
|
24
|
+
import type { ComponentDefinition, ComponentInfo } from './components.js';
|
|
25
|
+
|
|
26
|
+
let componentLoadingOperationUniqueId = 0;
|
|
27
|
+
|
|
28
|
+
function cloneTemplateIntoElement(
|
|
29
|
+
componentName: string,
|
|
30
|
+
componentDefinition: ComponentDefinition,
|
|
31
|
+
element: Node,
|
|
32
|
+
): void {
|
|
33
|
+
const template = componentDefinition.template;
|
|
34
|
+
if (!template) {
|
|
35
|
+
throw new Error("Component '" + componentName + "' has no template");
|
|
36
|
+
}
|
|
37
|
+
const clonedNodesArray = cloneNodes(template);
|
|
38
|
+
virtualSetChildren(element, clonedNodesArray);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createViewModel(
|
|
42
|
+
componentDefinition: ComponentDefinition,
|
|
43
|
+
componentParams: unknown,
|
|
44
|
+
componentInfo: ComponentInfo,
|
|
45
|
+
): unknown {
|
|
46
|
+
const factory = componentDefinition.createViewModel;
|
|
47
|
+
return factory
|
|
48
|
+
? factory.call(componentDefinition, componentParams, componentInfo)
|
|
49
|
+
: componentParams;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const componentHandler: BindingHandler = {
|
|
53
|
+
init(element, valueAccessor, _allBindings, _viewModel, bindingContext) {
|
|
54
|
+
let currentViewModel: unknown;
|
|
55
|
+
let currentLoadingOperationId: number | null;
|
|
56
|
+
let afterRenderSub: { dispose(): void } | null = null;
|
|
57
|
+
|
|
58
|
+
function disposeAssociatedComponentViewModel() {
|
|
59
|
+
const vm = currentViewModel as { dispose?: () => void } | null;
|
|
60
|
+
if (typeof vm?.dispose === 'function') {
|
|
61
|
+
vm.dispose();
|
|
62
|
+
}
|
|
63
|
+
if (afterRenderSub) {
|
|
64
|
+
afterRenderSub.dispose();
|
|
65
|
+
}
|
|
66
|
+
afterRenderSub = null;
|
|
67
|
+
currentViewModel = null;
|
|
68
|
+
currentLoadingOperationId = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const originalChildNodes = Array.from(virtualChildNodes(element));
|
|
72
|
+
|
|
73
|
+
virtualEmptyNode(element);
|
|
74
|
+
addDisposeCallback(element, disposeAssociatedComponentViewModel);
|
|
75
|
+
|
|
76
|
+
const computed = new Computed(() => {
|
|
77
|
+
const value = unwrapObservable(valueAccessor());
|
|
78
|
+
let componentName: string;
|
|
79
|
+
let componentParams: unknown;
|
|
80
|
+
|
|
81
|
+
if (typeof value === 'string') {
|
|
82
|
+
componentName = value;
|
|
83
|
+
} else {
|
|
84
|
+
const obj = value as { name: unknown; params?: unknown };
|
|
85
|
+
componentName = unwrapObservable(obj.name) as string;
|
|
86
|
+
componentParams = unwrapObservable(obj.params);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!componentName) {
|
|
90
|
+
throw new Error('No component name specified');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const asyncContext = bindingEvent.startPossiblyAsyncContentBinding(element, bindingContext);
|
|
94
|
+
|
|
95
|
+
const loadingOperationId = currentLoadingOperationId = ++componentLoadingOperationUniqueId;
|
|
96
|
+
|
|
97
|
+
components.get(componentName, (componentDefinition) => {
|
|
98
|
+
if (currentLoadingOperationId !== loadingOperationId) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
disposeAssociatedComponentViewModel();
|
|
103
|
+
|
|
104
|
+
if (!componentDefinition) {
|
|
105
|
+
throw new Error("Unknown component '" + componentName + "'");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
cloneTemplateIntoElement(componentName, componentDefinition, element);
|
|
109
|
+
|
|
110
|
+
const componentInfo: ComponentInfo = {
|
|
111
|
+
element,
|
|
112
|
+
templateNodes: originalChildNodes,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const componentViewModel = createViewModel(componentDefinition, componentParams, componentInfo);
|
|
116
|
+
|
|
117
|
+
if (componentViewModel && componentViewModel !== componentParams &&
|
|
118
|
+
typeof componentParams === 'object' && componentParams !== null) {
|
|
119
|
+
wireParams(componentViewModel as object, componentParams as Record<string, unknown>, element);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (componentViewModel && typeof (componentViewModel as Record<string, unknown>).onInit === 'function') {
|
|
123
|
+
((componentViewModel as Record<string, unknown>).onInit as () => void)();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const childBindingContext = asyncContext.createChildContext(componentViewModel, {
|
|
127
|
+
extend(ctx: BindingContext) {
|
|
128
|
+
ctx['$component'] = componentViewModel;
|
|
129
|
+
ctx['$componentTemplateNodes'] = originalChildNodes;
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (componentViewModel && typeof (componentViewModel as Record<string, unknown>).onDescendantsComplete === 'function') {
|
|
134
|
+
afterRenderSub = bindingEvent.subscribe(
|
|
135
|
+
element,
|
|
136
|
+
bindingEvent.descendantsComplete,
|
|
137
|
+
(componentViewModel as Record<string, unknown>).onDescendantsComplete as (value: unknown) => void,
|
|
138
|
+
componentViewModel,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
currentViewModel = componentViewModel;
|
|
143
|
+
ignore(() => applyBindingsToDescendants(childBindingContext, element));
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
addDisposeCallback(element, () => computed.dispose());
|
|
148
|
+
|
|
149
|
+
return { controlsDescendantBindings: true };
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
bindingHandlers['component'] = componentHandler;
|
|
154
|
+
allowedVirtualElementBindings['component'] = true;
|
|
155
|
+
|
|
156
|
+
// ---- Custom Element Bridge ----
|
|
157
|
+
|
|
158
|
+
export function getComponentNameForNode(node: Node): string | undefined {
|
|
159
|
+
if (node.nodeType !== 1) return undefined;
|
|
160
|
+
const tagNameLower = ((node as Element).tagName || '').toLowerCase();
|
|
161
|
+
if (!components.isRegistered(tagNameLower)) return undefined;
|
|
162
|
+
if (tagNameLower.indexOf('-') !== -1 || ('' + node) === '[object HTMLUnknownElement]') {
|
|
163
|
+
return tagNameLower;
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isWritable(value: unknown): boolean {
|
|
169
|
+
if (value instanceof Observable) return true;
|
|
170
|
+
if (value instanceof Computed) return (value as Computed<unknown>).hasWriteFunction;
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const nativeProviderInstance = new BindingProvider();
|
|
175
|
+
|
|
176
|
+
const CONTEXT_KEYWORDS = new Set([
|
|
177
|
+
'$data', '$rawData', '$root', '$parent', '$parentContext',
|
|
178
|
+
'$parents', '$component', '$componentTemplateNodes', '$index',
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
const OBSERVABLE_REF_PATTERN = /^\$([a-zA-Z_$][\w$]*)$/;
|
|
182
|
+
|
|
183
|
+
function getComponentParamsFromCustomElement(
|
|
184
|
+
elem: Element,
|
|
185
|
+
bindingContext: BindingContext,
|
|
186
|
+
): Record<string, unknown> {
|
|
187
|
+
const paramsAttribute = elem.getAttribute('params');
|
|
188
|
+
|
|
189
|
+
if (paramsAttribute) {
|
|
190
|
+
const keyValuePairs = parseObjectLiteral(paramsAttribute);
|
|
191
|
+
const observableRefParams: Record<string, string> = {};
|
|
192
|
+
const normalPairs: string[] = [];
|
|
193
|
+
|
|
194
|
+
for (const kv of keyValuePairs) {
|
|
195
|
+
const key = kv.key || kv.unknown || '';
|
|
196
|
+
const val = (kv.value || '').trim();
|
|
197
|
+
const match = val.match(OBSERVABLE_REF_PATTERN);
|
|
198
|
+
|
|
199
|
+
if (match && !CONTEXT_KEYWORDS.has(val)) {
|
|
200
|
+
observableRefParams[key] = match[1];
|
|
201
|
+
} else if (key) {
|
|
202
|
+
normalPairs.push(key + ':' + (kv.value || ''));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const result: Record<string, unknown> = {};
|
|
207
|
+
const rawParamComputedValues: Record<string, Computed<unknown>> = {};
|
|
208
|
+
|
|
209
|
+
if (normalPairs.length > 0) {
|
|
210
|
+
const normalParamsString = normalPairs.join(',');
|
|
211
|
+
const params = nativeProviderInstance.parseBindingsString(
|
|
212
|
+
normalParamsString,
|
|
213
|
+
bindingContext,
|
|
214
|
+
elem,
|
|
215
|
+
{ valueAccessors: true },
|
|
216
|
+
) as Record<string, () => unknown> | null;
|
|
217
|
+
|
|
218
|
+
if (params) {
|
|
219
|
+
for (const paramName of Object.keys(params)) {
|
|
220
|
+
rawParamComputedValues[paramName] = new Computed(
|
|
221
|
+
params[paramName] as () => unknown,
|
|
222
|
+
);
|
|
223
|
+
addDisposeCallback(elem, () => rawParamComputedValues[paramName].dispose());
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const paramName of Object.keys(rawParamComputedValues)) {
|
|
227
|
+
const paramValueComputed = rawParamComputedValues[paramName];
|
|
228
|
+
const paramValue = paramValueComputed.peek();
|
|
229
|
+
|
|
230
|
+
if (!paramValueComputed.isActive()) {
|
|
231
|
+
result[paramName] = paramValue;
|
|
232
|
+
} else {
|
|
233
|
+
result[paramName] = new Computed({
|
|
234
|
+
read() {
|
|
235
|
+
return unwrapObservable(paramValueComputed.get());
|
|
236
|
+
},
|
|
237
|
+
write: isWritable(paramValue)
|
|
238
|
+
? (value: unknown) => {
|
|
239
|
+
const currentObs = paramValueComputed.get();
|
|
240
|
+
if (currentObs instanceof Observable) {
|
|
241
|
+
currentObs.set(value);
|
|
242
|
+
} else if (currentObs instanceof Computed && (currentObs as Computed<unknown>).hasWriteFunction) {
|
|
243
|
+
(currentObs as Computed<unknown>).set(value);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
: undefined,
|
|
247
|
+
});
|
|
248
|
+
addDisposeCallback(elem, () => (result[paramName] as Computed<unknown>).dispose());
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (const [key, propName] of Object.entries(observableRefParams)) {
|
|
255
|
+
const dataContext = bindingContext.$data;
|
|
256
|
+
if (dataContext && typeof dataContext === 'object') {
|
|
257
|
+
const obs = getObservable(dataContext as object, propName);
|
|
258
|
+
if (obs) {
|
|
259
|
+
result[key] = obs;
|
|
260
|
+
} else {
|
|
261
|
+
result[key] = (dataContext as Record<string, unknown>)[propName];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!Object.prototype.hasOwnProperty.call(result, '$raw')) {
|
|
267
|
+
result['$raw'] = rawParamComputedValues;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { $raw: {} };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function addBindingsForCustomElement(
|
|
277
|
+
allBindings: Record<string, unknown> | null,
|
|
278
|
+
node: Node,
|
|
279
|
+
bindingContext: BindingContext,
|
|
280
|
+
valueAccessors?: boolean,
|
|
281
|
+
): Record<string, unknown> | null {
|
|
282
|
+
if (node.nodeType === 1) {
|
|
283
|
+
const componentName = getComponentNameForNode(node);
|
|
284
|
+
if (componentName) {
|
|
285
|
+
allBindings = allBindings || {};
|
|
286
|
+
|
|
287
|
+
if (allBindings['component']) {
|
|
288
|
+
throw new Error('Cannot use the "component" binding on a custom element matching a component');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const componentBindingValue = {
|
|
292
|
+
name: componentName,
|
|
293
|
+
params: getComponentParamsFromCustomElement(node as Element, bindingContext),
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
allBindings['component'] = valueAccessors
|
|
297
|
+
? () => componentBindingValue
|
|
298
|
+
: componentBindingValue;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return allBindings;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Register hooks on the binding provider to enable custom element detection
|
|
306
|
+
setCustomElementHooks(getComponentNameForNode, addBindingsForCustomElement);
|
|
307
|
+
|
|
308
|
+
// Apply display:contents to custom elements so they don't affect layout.
|
|
309
|
+
// Opt out per-element with the "layout" attribute, or globally via options.customElementDisplayContents.
|
|
310
|
+
addNodePreprocessor((node: Node): Node[] | void => {
|
|
311
|
+
if (!options.customElementDisplayContents) return;
|
|
312
|
+
if (node.nodeType === 1 && node.nodeName.includes('-')) {
|
|
313
|
+
const el = node as HTMLElement;
|
|
314
|
+
if (!el.hasAttribute('layout')) {
|
|
315
|
+
el.style.display = 'contents';
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|