-
Notifications
You must be signed in to change notification settings - Fork 382
/
upgrade.ts
147 lines (133 loc) · 5.19 KB
/
upgrade.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
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import {
assert,
assign,
fields,
isFunction,
isNull,
isObject,
isUndefined,
toString,
} from '@lwc/shared';
import {
createVM,
removeRootVM,
appendRootVM,
getAssociatedVM,
VMState,
getAssociatedVMIfPresent,
} from './vm';
import { ComponentConstructor } from './component';
import { EmptyObject, isCircularModuleDependency, resolveCircularModuleDependency } from './utils';
import { getComponentDef, setElementProto } from './def';
import { patchCustomElementWithRestrictions } from './restrictions';
import { GlobalMeasurementPhase, startGlobalMeasure, endGlobalMeasure } from './performance-timing';
import { appendChild, insertBefore, replaceChild, removeChild } from '../env/node';
const { createFieldName, getHiddenField, setHiddenField } = fields;
const ConnectingSlot = createFieldName('connecting', 'engine');
const DisconnectingSlot = createFieldName('disconnecting', 'engine');
function callNodeSlot(node: Node, slot: symbol): Node {
if (process.env.NODE_ENV !== 'production') {
assert.isTrue(node, `callNodeSlot() should not be called for a non-object`);
}
const fn = getHiddenField(node, slot) as Function | undefined;
if (!isUndefined(fn)) {
fn();
}
return node; // for convenience
}
// monkey patching Node methods to be able to detect the insertions and removal of
// root elements created via createElement.
assign(Node.prototype, {
appendChild(newChild: Node): Node {
const appendedNode = appendChild.call(this, newChild);
return callNodeSlot(appendedNode, ConnectingSlot);
},
insertBefore(newChild: Node, referenceNode: Node): Node {
const insertedNode = insertBefore.call(this, newChild, referenceNode);
return callNodeSlot(insertedNode, ConnectingSlot);
},
removeChild(oldChild: Node): Node {
const removedNode = removeChild.call(this, oldChild);
return callNodeSlot(removedNode, DisconnectingSlot);
},
replaceChild(newChild: Node, oldChild: Node): Node {
const replacedNode = replaceChild.call(this, newChild, oldChild);
callNodeSlot(replacedNode, DisconnectingSlot);
callNodeSlot(newChild, ConnectingSlot);
return replacedNode;
},
});
type ShadowDomMode = 'open' | 'closed';
interface CreateElementOptions {
is: ComponentConstructor;
mode?: ShadowDomMode;
}
/**
* EXPERIMENTAL: This function is almost identical to document.createElement
* (https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement)
* with the slightly difference that in the options, you can pass the `is`
* property set to a Constructor instead of just a string value. The intent
* is to allow the creation of an element controlled by LWC without having
* to register the element as a custom element. E.g.:
*
* const el = createElement('x-foo', { is: FooCtor });
*
* If the value of `is` attribute is not a constructor,
* then it throws a TypeError.
*/
export function createElement(sel: string, options: CreateElementOptions): HTMLElement {
if (!isObject(options) || isNull(options)) {
throw new TypeError(
`"createElement" function expects an object as second parameter but received "${toString(
options
)}".`
);
}
let Ctor = options.is;
if (!isFunction(Ctor)) {
throw new TypeError(
`"createElement" function expects a "is" option with a valid component constructor.`
);
}
const mode = options.mode !== 'closed' ? 'open' : 'closed';
// Create element with correct tagName
const element = document.createElement(sel);
if (!isUndefined(getAssociatedVMIfPresent(element))) {
// There is a possibility that a custom element is registered under tagName,
// in which case, the initialization is already carry on, and there is nothing else
// to do here.
return element;
}
if (isCircularModuleDependency(Ctor)) {
Ctor = resolveCircularModuleDependency(Ctor);
}
const def = getComponentDef(Ctor);
setElementProto(element, def);
if (process.env.NODE_ENV !== 'production') {
patchCustomElementWithRestrictions(element, EmptyObject);
}
// In case the element is not initialized already, we need to carry on the manual creation
createVM(element, Ctor, { mode, isRoot: true, owner: null });
// Handle insertion and removal from the DOM manually
setHiddenField(element, ConnectingSlot, () => {
const vm = getAssociatedVM(element);
startGlobalMeasure(GlobalMeasurementPhase.HYDRATE, vm);
if (vm.state === VMState.connected) {
// usually means moving the element from one place to another, which is observable via life-cycle hooks
removeRootVM(vm);
}
appendRootVM(vm);
endGlobalMeasure(GlobalMeasurementPhase.HYDRATE, vm);
});
setHiddenField(element, DisconnectingSlot, () => {
const vm = getAssociatedVM(element);
removeRootVM(vm);
});
return element;
}